forked from trent_larson/crowd-funder-for-time-pwa
Merge branch 'master' into gifting-ui-2025-05
This commit is contained in:
59
src/libs/capacitor/app.ts
Normal file
59
src/libs/capacitor/app.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Import from node_modules using relative path
|
||||
|
||||
import {
|
||||
App as CapacitorApp,
|
||||
AppLaunchUrl,
|
||||
BackButtonListener,
|
||||
} from "../../../node_modules/@capacitor/app";
|
||||
import type { PluginListenerHandle } from "@capacitor/core";
|
||||
|
||||
/**
|
||||
* Interface defining the app event listener functionality
|
||||
* Supports 'backButton' and 'appUrlOpen' events from Capacitor
|
||||
*/
|
||||
interface AppInterface {
|
||||
/**
|
||||
* Add listener for back button events
|
||||
* @param eventName - Must be 'backButton'
|
||||
* @param listenerFunc - Callback function for back button events
|
||||
* @returns Promise that resolves with a removable listener handle
|
||||
*/
|
||||
addListener(
|
||||
eventName: "backButton",
|
||||
listenerFunc: BackButtonListener,
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
|
||||
/**
|
||||
* Add listener for app URL open events
|
||||
* @param eventName - Must be 'appUrlOpen'
|
||||
* @param listenerFunc - Callback function for URL open events
|
||||
* @returns Promise that resolves with a removable listener handle
|
||||
*/
|
||||
addListener(
|
||||
eventName: "appUrlOpen",
|
||||
listenerFunc: (data: AppLaunchUrl) => void,
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* App wrapper for Capacitor functionality
|
||||
* Provides type-safe event listeners for back button and URL open events
|
||||
*/
|
||||
export const App: AppInterface = {
|
||||
addListener(
|
||||
eventName: "backButton" | "appUrlOpen",
|
||||
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle {
|
||||
if (eventName === "backButton") {
|
||||
return CapacitorApp.addListener(
|
||||
eventName,
|
||||
listenerFunc as BackButtonListener,
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
} else {
|
||||
return CapacitorApp.addListener(
|
||||
eventName,
|
||||
listenerFunc as (data: AppLaunchUrl) => void,
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createEndorserJwtForDid,
|
||||
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||
CONTACT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
} from "../../libs/endorserServer";
|
||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||
import { logger } from "../../utils/logger";
|
||||
@@ -31,7 +32,7 @@ export const newIdentifier = (
|
||||
publicHex: string,
|
||||
privateHex: string,
|
||||
derivationPath: string,
|
||||
): Omit<IIdentifier, keyof "provider"> => {
|
||||
): IIdentifier => {
|
||||
return {
|
||||
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
||||
keys: [
|
||||
@@ -104,34 +105,41 @@ export const accessToken = async (did?: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
@return payload of JWT pulled out of any recognized URL path (if any)
|
||||
* Extract JWT from various URL formats
|
||||
* @param jwtUrlText The URL containing the JWT
|
||||
* @returns The extracted JWT or null if not found
|
||||
*/
|
||||
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
|
||||
let jwtText = jwtUrlText;
|
||||
const appImportConfirmUrlLoc = jwtText.indexOf(
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
);
|
||||
if (appImportConfirmUrlLoc > -1) {
|
||||
jwtText = jwtText.substring(
|
||||
appImportConfirmUrlLoc +
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI.length,
|
||||
);
|
||||
try {
|
||||
let jwtText = jwtUrlText;
|
||||
|
||||
// Try to extract JWT from URL paths
|
||||
const paths = [
|
||||
CONTACT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const pathIndex = jwtText.indexOf(path);
|
||||
if (pathIndex > -1) {
|
||||
jwtText = jwtText.substring(pathIndex + path.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JWT format
|
||||
if (!jwtText.match(/^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/)) {
|
||||
logger.error("Invalid JWT format in URL:", jwtUrlText);
|
||||
return null;
|
||||
}
|
||||
|
||||
return jwtText;
|
||||
} catch (error) {
|
||||
logger.error("Error extracting JWT from URL:", error);
|
||||
return null;
|
||||
}
|
||||
const appImportOneUrlLoc = jwtText.indexOf(
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||
);
|
||||
if (appImportOneUrlLoc > -1) {
|
||||
jwtText = jwtText.substring(
|
||||
appImportOneUrlLoc + CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI.length,
|
||||
);
|
||||
}
|
||||
const endorserUrlPathLoc = jwtText.indexOf(CONTACT_URL_PATH_ENDORSER_CH_OLD);
|
||||
if (endorserUrlPathLoc > -1) {
|
||||
jwtText = jwtText.substring(
|
||||
endorserUrlPathLoc + CONTACT_URL_PATH_ENDORSER_CH_OLD.length,
|
||||
);
|
||||
}
|
||||
return jwtText;
|
||||
};
|
||||
|
||||
export const nextDerivationPath = (origDerivPath: string) => {
|
||||
@@ -151,7 +159,7 @@ export const nextDerivationPath = (origDerivPath: string) => {
|
||||
};
|
||||
|
||||
// Base64 encoding/decoding utilities for browser
|
||||
function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
export function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
@@ -160,7 +168,7 @@ function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return btoa(binary);
|
||||
}
|
||||
@@ -170,7 +178,7 @@ const IV_LENGTH = 12;
|
||||
const KEY_LENGTH = 256;
|
||||
const ITERATIONS = 100000;
|
||||
|
||||
// Encryption helper function
|
||||
// Message encryption helper function, used for onboarding meeting messages
|
||||
export async function encryptMessage(message: string, password: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
@@ -218,7 +226,7 @@ export async function encryptMessage(message: string, password: string) {
|
||||
return btoa(JSON.stringify(result));
|
||||
}
|
||||
|
||||
// Decryption helper function
|
||||
// Message decryption helper function, used for onboarding meeting messages
|
||||
export async function decryptMessage(encryptedJson: string, password: string) {
|
||||
const decoder = new TextDecoder();
|
||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
||||
@@ -265,7 +273,7 @@ export async function decryptMessage(encryptedJson: string, password: string) {
|
||||
}
|
||||
|
||||
// Test function to verify encryption/decryption
|
||||
export async function testEncryptionDecryption() {
|
||||
export async function testMessageEncryptionDecryption() {
|
||||
try {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testPassword = "myTestPassword123";
|
||||
@@ -291,9 +299,111 @@ export async function testEncryptionDecryption() {
|
||||
logger.log("\nTesting with wrong password...");
|
||||
try {
|
||||
await decryptMessage(encrypted, "wrongPassword");
|
||||
logger.log("Should not reach here");
|
||||
logger.log("Incorrectly decrypted with wrong password ❌");
|
||||
} catch (error) {
|
||||
logger.log("Correctly failed with wrong password ✅");
|
||||
logger.log("Correctly failed to decrypt with wrong password ✅");
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error("Test failed with error:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
|
||||
export async function simpleEncrypt(
|
||||
text: string,
|
||||
secret: ArrayBuffer,
|
||||
): Promise<ArrayBuffer> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
// Derive a 256-bit key from the secret using SHA-256
|
||||
const keyData = await crypto.subtle.digest("SHA-256", secret);
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["encrypt"],
|
||||
);
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
new TextEncoder().encode(text),
|
||||
);
|
||||
|
||||
// Combine IV and encrypted data
|
||||
const result = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
result.set(iv);
|
||||
result.set(new Uint8Array(encrypted), iv.length);
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
|
||||
export async function simpleDecrypt(
|
||||
encryptedText: ArrayBuffer,
|
||||
secret: ArrayBuffer,
|
||||
): Promise<string> {
|
||||
const data = new Uint8Array(encryptedText);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
const iv = data.slice(0, 16);
|
||||
const encrypted = data.slice(16);
|
||||
|
||||
// Derive the same 256-bit key from the secret using SHA-256
|
||||
const keyData = await crypto.subtle.digest("SHA-256", secret);
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
encrypted,
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// Test function for simple encryption/decryption
|
||||
export async function testSimpleEncryptionDecryption() {
|
||||
try {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testSecret = crypto.getRandomValues(new Uint8Array(32));
|
||||
|
||||
logger.log("Original message:", testMessage);
|
||||
|
||||
// Test encryption
|
||||
logger.log("Encrypting...");
|
||||
const encrypted = await simpleEncrypt(testMessage, testSecret);
|
||||
const encryptedBase64 = arrayBufferToBase64(encrypted);
|
||||
logger.log("Encrypted result:", encryptedBase64);
|
||||
|
||||
// Test decryption
|
||||
logger.log("Decrypting...");
|
||||
const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
|
||||
const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
|
||||
logger.log("Decrypted result:", decrypted);
|
||||
|
||||
// Verify
|
||||
const success = testMessage === decrypted;
|
||||
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||
logger.log("Messages match:", success);
|
||||
|
||||
// Test with wrong secret
|
||||
logger.log("\nTesting with wrong secret...");
|
||||
try {
|
||||
await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
|
||||
logger.log("Incorrectly decrypted with wrong secret ❌");
|
||||
} catch (error) {
|
||||
logger.log("Correctly failed to decrypt with wrong secret ✅");
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
@@ -17,29 +17,12 @@ import { didEthLocalResolver } from "./did-eth-local-resolver";
|
||||
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
||||
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
||||
import { urlBase64ToUint8Array } from "./util";
|
||||
import { KeyMeta, KeyMetaWithPrivate } from "../../../interfaces/common";
|
||||
|
||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
||||
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
||||
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
||||
|
||||
/**
|
||||
* Meta info about a key
|
||||
*/
|
||||
export interface KeyMeta {
|
||||
/**
|
||||
* Decentralized ID for the key
|
||||
*/
|
||||
did: string;
|
||||
/**
|
||||
* Stringified IIDentifier object from Veramo
|
||||
*/
|
||||
identity?: string;
|
||||
/**
|
||||
* The Webauthn credential ID in hex, if this is from a passkey
|
||||
*/
|
||||
passkeyCredIdHex?: string;
|
||||
}
|
||||
|
||||
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
||||
|
||||
/**
|
||||
@@ -51,7 +34,7 @@ export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
||||
}
|
||||
|
||||
export async function createEndorserJwtForKey(
|
||||
account: KeyMeta,
|
||||
account: KeyMetaWithPrivate,
|
||||
payload: object,
|
||||
expiresIn?: number,
|
||||
) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Buffer } from "buffer/";
|
||||
import { JWTPayload } from "did-jwt";
|
||||
import { DIDResolutionResult } from "did-resolver";
|
||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||
import { p256 } from "@noble/curves/p256";
|
||||
import {
|
||||
startAuthentication,
|
||||
startRegistration,
|
||||
@@ -11,12 +10,13 @@ import {
|
||||
generateRegistrationOptions,
|
||||
verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse,
|
||||
VerifyAuthenticationResponseOpts,
|
||||
} from "@simplewebauthn/server";
|
||||
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
|
||||
import {
|
||||
Base64URLString,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
AuthenticatorAssertionResponse,
|
||||
} from "@simplewebauthn/types";
|
||||
|
||||
import { AppString } from "../../../constants/app";
|
||||
@@ -194,16 +194,19 @@ export class PeerSetup {
|
||||
},
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.get(options);
|
||||
const credential = (await navigator.credentials.get(
|
||||
options,
|
||||
)) as PublicKeyCredential;
|
||||
// console.log("nav credential get", credential);
|
||||
|
||||
this.authenticatorData = credential?.response.authenticatorData;
|
||||
const response = credential?.response as AuthenticatorAssertionResponse;
|
||||
this.authenticatorData = response?.authenticatorData;
|
||||
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
||||
this.authenticatorData as ArrayBuffer,
|
||||
);
|
||||
|
||||
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
|
||||
credential?.response.clientDataJSON,
|
||||
response?.clientDataJSON,
|
||||
);
|
||||
|
||||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||
@@ -228,9 +231,7 @@ export class PeerSetup {
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
|
||||
const origSignature = Buffer.from(credential?.response.signature).toString(
|
||||
"base64",
|
||||
);
|
||||
const origSignature = Buffer.from(response?.signature).toString("base64");
|
||||
this.signature = origSignature
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
@@ -315,24 +316,18 @@ export async function createDidPeerJwt(
|
||||
// ... and this import:
|
||||
// import { p256 } from "@noble/curves/p256";
|
||||
export async function verifyJwtP256(
|
||||
credIdHex: string,
|
||||
issuerDid: string,
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
) {
|
||||
const authDataFromBase = Buffer.from(authenticatorData);
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
// Use challenge in preimage construction
|
||||
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
|
||||
|
||||
const isValid = p256.verify(
|
||||
finalSigBuffer,
|
||||
@@ -383,122 +378,37 @@ export async function verifyJwtSimplewebauthn(
|
||||
|
||||
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
||||
export async function verifyJwtWebCrypto(
|
||||
credId: Base64URLString,
|
||||
issuerDid: string,
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
) {
|
||||
const authDataFromBase = Buffer.from(authenticatorData);
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
// Use challenge in preimage construction
|
||||
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||
if (!did.startsWith("did:peer:0z")) {
|
||||
throw new Error(
|
||||
"This only verifies a peer DID, method 0, encoded base58btc.",
|
||||
);
|
||||
}
|
||||
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
||||
// (another reference is the @aviarytech/did-peer resolver)
|
||||
// Remove unused functions:
|
||||
// - peerDidToDidDocument
|
||||
// - COSEtoPEM
|
||||
// - base64urlDecodeArrayBuffer
|
||||
// - base64urlEncodeArrayBuffer
|
||||
// - pemToCryptoKey
|
||||
|
||||
/**
|
||||
* Looks like JsonWebKey2020 isn't too difficult:
|
||||
* - change context security/suites link to jws-2020/v1
|
||||
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
|
||||
* - change type to JsonWebKey2020
|
||||
*/
|
||||
|
||||
const id = did.split(":")[2];
|
||||
const multibase = id.slice(1);
|
||||
const encnumbasis = multibase.slice(1);
|
||||
const didDocument = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/secp256k1-2019/v1",
|
||||
],
|
||||
assertionMethod: [did + "#" + encnumbasis],
|
||||
authentication: [did + "#" + encnumbasis],
|
||||
capabilityDelegation: [did + "#" + encnumbasis],
|
||||
capabilityInvocation: [did + "#" + encnumbasis],
|
||||
id: did,
|
||||
keyAgreement: undefined,
|
||||
service: undefined,
|
||||
verificationMethod: [
|
||||
{
|
||||
controller: did,
|
||||
id: did + "#" + encnumbasis,
|
||||
publicKeyMultibase: multibase,
|
||||
type: "EcdsaSecp256k1VerificationKey2019",
|
||||
},
|
||||
],
|
||||
};
|
||||
return {
|
||||
didDocument,
|
||||
didDocumentMetadata: {},
|
||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
||||
};
|
||||
}
|
||||
|
||||
// convert COSE public key to PEM format
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function COSEtoPEM(cose: Buffer) {
|
||||
// const alg = cose.get(3); // Algorithm
|
||||
const x = cose[-2]; // x-coordinate
|
||||
const y = cose[-3]; // y-coordinate
|
||||
|
||||
// Ensure the coordinates are in the correct format
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error because it complains about the type of x and y
|
||||
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
||||
|
||||
// Convert to PEM format
|
||||
const pem = `-----BEGIN PUBLIC KEY-----
|
||||
${pubKeyBuffer.toString("base64")}
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
||||
// tried the base64url library but got an error using their Buffer
|
||||
// Keep only the used functions:
|
||||
export function base64urlDecodeString(input: string) {
|
||||
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
|
||||
}
|
||||
|
||||
// tried the base64url library but got an error using their Buffer
|
||||
export function base64urlEncodeString(input: string) {
|
||||
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function base64urlDecodeArrayBuffer(input: string) {
|
||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
||||
const str = atob(input + pad);
|
||||
const bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return base64urlEncodeString(str);
|
||||
}
|
||||
|
||||
// from @simplewebauthn/browser
|
||||
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
@@ -523,28 +433,3 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function pemToCryptoKey(pem: string) {
|
||||
const binaryDerString = atob(
|
||||
pem
|
||||
.split("\n")
|
||||
.filter((x) => !x.includes("-----"))
|
||||
.join(""),
|
||||
);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
}
|
||||
// console.log("binaryDer", binaryDer.buffer);
|
||||
return await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
binaryDer.buffer,
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
true,
|
||||
["verify"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,29 +26,42 @@ import {
|
||||
DEFAULT_IMAGE_API_SERVER,
|
||||
NotificationIface,
|
||||
APP_SERVER,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
|
||||
import { logConsoleAndDb, NonsensitiveDexie } from "../db/index";
|
||||
|
||||
import { NonsensitiveDexie } from "../db/index";
|
||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||
import {
|
||||
retrieveAccountMetadata,
|
||||
retrieveFullyDecryptedAccount,
|
||||
getPasskeyExpirationSeconds,
|
||||
} from "../libs/util";
|
||||
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
|
||||
import { createEndorserJwtForKey } from "../libs/crypto/vc";
|
||||
import {
|
||||
GiveActionClaim,
|
||||
JoinActionClaim,
|
||||
OfferClaim,
|
||||
PlanActionClaim,
|
||||
RegisterActionClaim,
|
||||
TenureClaim,
|
||||
} from "../interfaces/claims";
|
||||
|
||||
import {
|
||||
GiveVerifiableCredential,
|
||||
OfferVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
GenericVerifiableCredential,
|
||||
GenericCredWrapper,
|
||||
PlanSummaryRecord,
|
||||
GenericVerifiableCredential,
|
||||
AxiosErrorResponse,
|
||||
UserInfo,
|
||||
CreateAndSubmitClaimResult,
|
||||
} from "../interfaces";
|
||||
ClaimObject,
|
||||
VerifiableCredentialClaim,
|
||||
QuantitativeValue,
|
||||
KeyMetaWithPrivate,
|
||||
KeyMetaMaybeWithPrivate,
|
||||
} from "../interfaces/common";
|
||||
import { PlanSummaryRecord } from "../interfaces/records";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
/**
|
||||
* Standard context for schema.org data
|
||||
@@ -86,6 +99,12 @@ export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt=";
|
||||
*/
|
||||
export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
|
||||
|
||||
/**
|
||||
* URL path suffix for contact confirmation
|
||||
* @constant {string}
|
||||
*/
|
||||
export const CONTACT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact/confirm/";
|
||||
|
||||
/**
|
||||
* The prefix for handle IDs, the permanent ID for claims on Endorser
|
||||
* @constant {string}
|
||||
@@ -94,7 +113,10 @@ export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||
|
||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||
{
|
||||
claim: { "@type": "" },
|
||||
claim: {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
"@type": "",
|
||||
},
|
||||
handleId: "",
|
||||
id: "",
|
||||
issuedAt: "",
|
||||
@@ -119,7 +141,7 @@ export function isDid(did: string): boolean {
|
||||
* @param {string} did - The DID to check
|
||||
* @returns {boolean} True if DID is hidden
|
||||
*/
|
||||
export function isHiddenDid(did: string): boolean {
|
||||
export function isHiddenDid(did: string | undefined): boolean {
|
||||
return did === HIDDEN_DID;
|
||||
}
|
||||
|
||||
@@ -174,37 +196,21 @@ export function isEmptyOrHiddenDid(did?: string): boolean {
|
||||
* };
|
||||
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
|
||||
*/
|
||||
function testRecursivelyOnStrings(
|
||||
func: (arg0: unknown) => boolean,
|
||||
const testRecursivelyOnStrings = (
|
||||
input: unknown,
|
||||
): boolean {
|
||||
// Test direct string values
|
||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
||||
return func(input);
|
||||
test: (s: string) => boolean,
|
||||
): boolean => {
|
||||
if (typeof input === "string") {
|
||||
return test(input);
|
||||
} else if (Array.isArray(input)) {
|
||||
return input.some((item) => testRecursivelyOnStrings(item, test));
|
||||
} else if (input && typeof input === "object") {
|
||||
return Object.values(input as Record<string, unknown>).some((value) =>
|
||||
testRecursivelyOnStrings(value, test),
|
||||
);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function containsHiddenDid(obj: any) {
|
||||
@@ -545,7 +551,11 @@ export async function setPlanInCache(
|
||||
* @returns {string|undefined} User-friendly message or undefined if none found
|
||||
*/
|
||||
export function serverMessageForUser(error: unknown): string | undefined {
|
||||
return error?.response?.data?.error?.message;
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const err = error as AxiosErrorResponse;
|
||||
return err.response?.data?.error?.message;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -567,18 +577,27 @@ export function errorStringForLog(error: unknown) {
|
||||
// --- 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;
|
||||
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorResponseText = JSON.stringify(err.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 (
|
||||
err.response?.config &&
|
||||
err.config &&
|
||||
R.equals(err.config, err.response.config)
|
||||
) {
|
||||
// but exclude "config" because it's already in there
|
||||
const newErrorResponseText = JSON.stringify(
|
||||
R.omit(["config"] as never[], err.response),
|
||||
);
|
||||
fullError +=
|
||||
" - .response w/o same config JSON: " + newErrorResponseText;
|
||||
} else {
|
||||
fullError += " - .response JSON: " + errorResponseText;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fullError;
|
||||
@@ -636,7 +655,7 @@ export async function getNewOffersToUserProjects(
|
||||
* @param lastClaimId supplied when editing a previous claim
|
||||
*/
|
||||
export function hydrateGive(
|
||||
vcClaimOrig?: GiveVerifiableCredential,
|
||||
vcClaimOrig?: GiveActionClaim,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
description?: string,
|
||||
@@ -648,10 +667,8 @@ export function hydrateGive(
|
||||
imageUrl?: string,
|
||||
providerPlanHandleId?: string,
|
||||
lastClaimId?: string,
|
||||
): GiveVerifiableCredential {
|
||||
// Remember: replace values or erase if it's null
|
||||
|
||||
const vcClaim: GiveVerifiableCredential = vcClaimOrig
|
||||
): GiveActionClaim {
|
||||
const vcClaim: GiveActionClaim = vcClaimOrig
|
||||
? R.clone(vcClaimOrig)
|
||||
: {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
@@ -659,55 +676,71 @@ export function hydrateGive(
|
||||
};
|
||||
|
||||
if (lastClaimId) {
|
||||
// this is an edit
|
||||
vcClaim.lastClaimId = lastClaimId;
|
||||
delete vcClaim.identifier;
|
||||
}
|
||||
|
||||
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||
if (fromDid) {
|
||||
vcClaim.agent = { identifier: fromDid };
|
||||
}
|
||||
if (toDid) {
|
||||
vcClaim.recipient = { identifier: toDid };
|
||||
}
|
||||
vcClaim.description = description || undefined;
|
||||
vcClaim.object =
|
||||
amount && !isNaN(amount)
|
||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||
: undefined;
|
||||
|
||||
// ensure fulfills is an array
|
||||
if (amount && !isNaN(amount)) {
|
||||
const quantitativeValue: QuantitativeValue = {
|
||||
amountOfThisGood: amount,
|
||||
unitCode: unitCode || "HUR",
|
||||
};
|
||||
vcClaim.object = quantitativeValue;
|
||||
}
|
||||
|
||||
// Initialize fulfills array if not present
|
||||
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.
|
||||
|
||||
// Filter and add fulfills elements
|
||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||
(elem) => elem["@type"] !== "PlanAction",
|
||||
(elem: { "@type": string }) => elem["@type"] !== "PlanAction",
|
||||
);
|
||||
|
||||
if (fulfillsProjectHandleId) {
|
||||
vcClaim.fulfills.push({
|
||||
"@type": "PlanAction",
|
||||
identifier: fulfillsProjectHandleId,
|
||||
});
|
||||
}
|
||||
|
||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||
(elem) => elem["@type"] !== "Offer",
|
||||
(elem: { "@type": string }) => 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": string }) =>
|
||||
elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction",
|
||||
);
|
||||
vcClaim.fulfills.push({ "@type": isTrade ? "TradeAction" : "DonateAction" });
|
||||
|
||||
vcClaim.fulfills.push({
|
||||
"@type": isTrade ? "TradeAction" : "DonateAction",
|
||||
});
|
||||
|
||||
vcClaim.image = imageUrl || undefined;
|
||||
|
||||
vcClaim.provider = providerPlanHandleId
|
||||
? { "@type": "PlanAction", identifier: providerPlanHandleId }
|
||||
: undefined;
|
||||
if (providerPlanHandleId) {
|
||||
vcClaim.provider = {
|
||||
"@type": "PlanAction",
|
||||
identifier: providerPlanHandleId,
|
||||
};
|
||||
}
|
||||
|
||||
return vcClaim;
|
||||
}
|
||||
@@ -731,7 +764,7 @@ export async function createAndSubmitGive(
|
||||
unitCode?: string,
|
||||
fulfillsProjectHandleId?: string,
|
||||
fulfillsOfferHandleId?: string,
|
||||
isTrade: boolean = false,
|
||||
isTrade: boolean = false, // remove, because this app is all for gifting
|
||||
imageUrl?: string,
|
||||
providerPlanHandleId?: string,
|
||||
): Promise<CreateAndSubmitClaimResult> {
|
||||
@@ -768,7 +801,7 @@ export async function createAndSubmitGive(
|
||||
export async function editAndSubmitGive(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
fullClaim: GenericCredWrapper<GiveVerifiableCredential>,
|
||||
fullClaim: GenericCredWrapper<GiveActionClaim>,
|
||||
issuerDid: string,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
@@ -809,7 +842,7 @@ export async function editAndSubmitGive(
|
||||
* @param lastClaimId supplied when editing a previous claim
|
||||
*/
|
||||
export function hydrateOffer(
|
||||
vcClaimOrig?: OfferVerifiableCredential,
|
||||
vcClaimOrig?: OfferClaim,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
itemDescription?: string,
|
||||
@@ -819,10 +852,8 @@ export function hydrateOffer(
|
||||
fulfillsProjectHandleId?: string,
|
||||
validThrough?: string,
|
||||
lastClaimId?: string,
|
||||
): OfferVerifiableCredential {
|
||||
// Remember: replace values or erase if it's null
|
||||
|
||||
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
||||
): OfferClaim {
|
||||
const vcClaim: OfferClaim = vcClaimOrig
|
||||
? R.clone(vcClaimOrig)
|
||||
: {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
@@ -835,14 +866,20 @@ export function hydrateOffer(
|
||||
delete vcClaim.identifier;
|
||||
}
|
||||
|
||||
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
|
||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||
if (fromDid) {
|
||||
vcClaim.offeredBy = { identifier: fromDid };
|
||||
}
|
||||
if (toDid) {
|
||||
vcClaim.recipient = { identifier: toDid };
|
||||
}
|
||||
vcClaim.description = conditionDescription || undefined;
|
||||
|
||||
vcClaim.includesObject =
|
||||
amount && !isNaN(amount)
|
||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||
: undefined;
|
||||
if (amount && !isNaN(amount)) {
|
||||
vcClaim.includesObject = {
|
||||
amountOfThisGood: amount,
|
||||
unitCode: unitCode || "HUR",
|
||||
};
|
||||
}
|
||||
|
||||
if (itemDescription || fulfillsProjectHandleId) {
|
||||
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
||||
@@ -854,6 +891,7 @@ export function hydrateOffer(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
vcClaim.validThrough = validThrough || undefined;
|
||||
|
||||
return vcClaim;
|
||||
@@ -893,7 +931,7 @@ export async function createAndSubmitOffer(
|
||||
undefined,
|
||||
);
|
||||
return createAndSubmitClaim(
|
||||
vcClaim as OfferVerifiableCredential,
|
||||
vcClaim as OfferClaim,
|
||||
issuerDid,
|
||||
apiServer,
|
||||
axios,
|
||||
@@ -903,7 +941,7 @@ export async function createAndSubmitOffer(
|
||||
export async function editAndSubmitOffer(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||
fullClaim: GenericCredWrapper<OfferClaim>,
|
||||
issuerDid: string,
|
||||
itemDescription: string,
|
||||
amount?: number,
|
||||
@@ -926,7 +964,7 @@ export async function editAndSubmitOffer(
|
||||
fullClaim.id,
|
||||
);
|
||||
return createAndSubmitClaim(
|
||||
vcClaim as OfferVerifiableCredential,
|
||||
vcClaim as OfferClaim,
|
||||
issuerDid,
|
||||
apiServer,
|
||||
axios,
|
||||
@@ -982,26 +1020,25 @@ export async function createAndSubmitClaim(
|
||||
},
|
||||
});
|
||||
|
||||
return { type: "success", response };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
return { success: true, handleId: response.data?.handleId };
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error submitting claim:", error);
|
||||
const errorMessage: string =
|
||||
serverMessageForUser(error) ||
|
||||
error.message ||
|
||||
(error && typeof error === "object" && "message" in error
|
||||
? String(error.message)
|
||||
: undefined) ||
|
||||
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
||||
|
||||
return {
|
||||
type: "error",
|
||||
error: {
|
||||
error: errorMessage,
|
||||
},
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateEndorserJwtUrlForAccount(
|
||||
account: KeyMeta,
|
||||
account: KeyMetaMaybeWithPrivate,
|
||||
isRegistered: boolean,
|
||||
givenName: string,
|
||||
profileImageUrl: string,
|
||||
@@ -1025,12 +1062,9 @@ export async function generateEndorserJwtUrlForAccount(
|
||||
}
|
||||
|
||||
// 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];
|
||||
if (isContact && account.derivationPath && account.mnemonic) {
|
||||
const newDerivPath = nextDerivationPath(account.derivationPath);
|
||||
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
||||
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
||||
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
||||
const nextPublicEncKeyHashBase64 =
|
||||
@@ -1050,7 +1084,11 @@ export async function createEndorserJwtForDid(
|
||||
expiresIn?: number,
|
||||
) {
|
||||
const account = await retrieveFullyDecryptedAccount(issuerDid);
|
||||
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn);
|
||||
return createEndorserJwtForKey(
|
||||
account as KeyMetaWithPrivate,
|
||||
payload,
|
||||
expiresIn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1098,21 +1136,21 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
||||
|
||||
similar code is also contained in endorser-mobile
|
||||
**/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claimSummary = (
|
||||
claim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
claim:
|
||||
| GenericVerifiableCredential
|
||||
| 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;
|
||||
let specificClaim: GenericVerifiableCredential;
|
||||
if ("claim" in claim) {
|
||||
// It's a GenericCredWrapper
|
||||
specificClaim = claim.claim as GenericVerifiableCredential;
|
||||
} else {
|
||||
// It's already a GenericVerifiableCredential
|
||||
specificClaim = claim;
|
||||
}
|
||||
if (Array.isArray(specificClaim)) {
|
||||
if (specificClaim.length === 1) {
|
||||
@@ -1147,88 +1185,112 @@ export const claimSpecialDescription = (
|
||||
identifiers: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
) => {
|
||||
let claim = record.claim;
|
||||
if (claim.claim) {
|
||||
// it's probably a Verified Credential
|
||||
claim = claim.claim;
|
||||
let claim:
|
||||
| GenericVerifiableCredential
|
||||
| GenericCredWrapper<GenericVerifiableCredential> = record.claim;
|
||||
if ("claim" in claim) {
|
||||
// it's a nested GenericCredWrapper
|
||||
claim = claim.claim as GenericVerifiableCredential;
|
||||
}
|
||||
|
||||
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
||||
const type = claim["@type"] || "UnknownType";
|
||||
|
||||
if (type === "AgreeAction") {
|
||||
return issuer + " agreed with " + claimSummary(claim.object);
|
||||
return (
|
||||
issuer +
|
||||
" agreed with " +
|
||||
claimSummary(claim.object as GenericVerifiableCredential)
|
||||
);
|
||||
} else if (isAccept(claim)) {
|
||||
return issuer + " accepted " + claimSummary(claim.object);
|
||||
return (
|
||||
issuer +
|
||||
" accepted " +
|
||||
claimSummary(claim.object as GenericVerifiableCredential)
|
||||
);
|
||||
} else if (type === "GiveAction") {
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const giver = claim.agent?.identifier || claim.agent?.did;
|
||||
const giveClaim = claim as GiveActionClaim;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyGiverDid = giveClaim.agent?.did;
|
||||
const giver = giveClaim.agent?.identifier || legacyGiverDid;
|
||||
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
|
||||
let gaveAmount = claim.object?.amountOfThisGood
|
||||
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||
let gaveAmount = giveClaim.object?.amountOfThisGood
|
||||
? displayAmount(
|
||||
giveClaim.object.unitCode as string,
|
||||
giveClaim.object.amountOfThisGood as number,
|
||||
)
|
||||
: "";
|
||||
if (claim.description) {
|
||||
if (giveClaim.description) {
|
||||
if (gaveAmount) {
|
||||
gaveAmount = gaveAmount + ", and also: ";
|
||||
}
|
||||
gaveAmount = gaveAmount + claim.description;
|
||||
gaveAmount = gaveAmount + giveClaim.description;
|
||||
}
|
||||
if (!gaveAmount) {
|
||||
gaveAmount = "something not described";
|
||||
}
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyRecipDid = giveClaim.recipient?.did;
|
||||
const gaveRecipientId = giveClaim.recipient?.identifier || legacyRecipDid;
|
||||
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 joinClaim = claim as JoinActionClaim;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyDid = joinClaim.agent?.did;
|
||||
const agent = joinClaim.agent?.identifier || legacyDid;
|
||||
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
|
||||
|
||||
let eventOrganizer =
|
||||
claim.event && claim.event.organizer && claim.event.organizer.name;
|
||||
joinClaim.event &&
|
||||
joinClaim.event.organizer &&
|
||||
joinClaim.event.organizer.name;
|
||||
eventOrganizer = eventOrganizer || "";
|
||||
let eventName = claim.event && claim.event.name;
|
||||
let eventName = joinClaim.event && joinClaim.event.name;
|
||||
eventName = eventName ? " " + eventName : "";
|
||||
let fullEvent = eventOrganizer + eventName;
|
||||
fullEvent = fullEvent ? " attended the " + fullEvent : "";
|
||||
|
||||
let eventDate = claim.event && claim.event.startTime;
|
||||
let eventDate = joinClaim.event && joinClaim.event.startTime;
|
||||
eventDate = eventDate ? " at " + eventDate : "";
|
||||
return contactInfo + fullEvent + eventDate;
|
||||
} else if (isOffer(claim)) {
|
||||
const offerer = claim.offeredBy?.identifier;
|
||||
const offerClaim = claim as OfferClaim;
|
||||
const offerer = offerClaim.offeredBy?.identifier;
|
||||
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
|
||||
let offering = "";
|
||||
if (claim.includesObject) {
|
||||
if (offerClaim.includesObject) {
|
||||
offering +=
|
||||
" " +
|
||||
displayAmount(
|
||||
claim.includesObject.unitCode,
|
||||
claim.includesObject.amountOfThisGood,
|
||||
offerClaim.includesObject.unitCode,
|
||||
offerClaim.includesObject.amountOfThisGood,
|
||||
);
|
||||
}
|
||||
if (claim.itemOffered?.description) {
|
||||
offering += ", saying: " + claim.itemOffered?.description;
|
||||
if (offerClaim.itemOffered?.description) {
|
||||
offering += ", saying: " + offerClaim.itemOffered?.description;
|
||||
}
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const offerRecipientId =
|
||||
claim.recipient?.identifier || claim.recipient?.did;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyDid = offerClaim.recipient?.did;
|
||||
const offerRecipientId = offerClaim.recipient?.identifier || legacyDid;
|
||||
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 planClaim = claim as PlanActionClaim;
|
||||
const claimer = planClaim.agent?.identifier || record.issuer;
|
||||
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||
return claimerInfo + " announced a project: " + claim.name;
|
||||
return claimerInfo + " announced a project: " + planClaim.name;
|
||||
} else if (type === "Tenure") {
|
||||
// party.did is for legacy data, before March 2023
|
||||
const claimer = claim.party?.identifier || claim.party?.did;
|
||||
const tenureClaim = claim as TenureClaim;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyDid = tenureClaim.party?.did;
|
||||
const claimer = tenureClaim.party?.identifier || legacyDid;
|
||||
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||
const polygon = claim.spatialUnit?.geo?.polygon || "";
|
||||
const polygon = tenureClaim.spatialUnit?.geo?.polygon || "";
|
||||
return (
|
||||
contactInfo +
|
||||
" possesses [" +
|
||||
@@ -1236,11 +1298,7 @@ export const claimSpecialDescription = (
|
||||
"...]"
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
issuer +
|
||||
" declared " +
|
||||
claimSummary(claim as GenericCredWrapper<GenericVerifiableCredential>)
|
||||
);
|
||||
return issuer + " declared " + claimSummary(claim);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1280,32 +1338,42 @@ export async function createEndorserJwtVcFromClaim(
|
||||
return createEndorserJwtForDid(issuerDid, vcPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JWT for a RegisterAction claim.
|
||||
*
|
||||
* @param activeDid - The DID of the user creating the invite
|
||||
* @param contact - The contact to register, with a 'did' field (all optional for invites)
|
||||
* @param identifier - The identifier for the invite, usually random
|
||||
* @param expiresIn - The number of seconds until the invite expires
|
||||
* @returns The JWT for the RegisterAction claim
|
||||
*/
|
||||
export async function createInviteJwt(
|
||||
activeDid: string,
|
||||
contact?: Contact,
|
||||
inviteId?: string,
|
||||
expiresIn?: number,
|
||||
identifier?: string,
|
||||
expiresIn?: number, // in seconds
|
||||
): Promise<string> {
|
||||
const vcClaim: RegisterVerifiableCredential = {
|
||||
const vcClaim: RegisterActionClaim = {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
"@type": "RegisterAction",
|
||||
agent: { identifier: activeDid },
|
||||
object: SERVICE_ID,
|
||||
identifier: identifier,
|
||||
};
|
||||
if (contact) {
|
||||
if (contact?.did) {
|
||||
vcClaim.participant = { identifier: contact.did };
|
||||
}
|
||||
if (inviteId) {
|
||||
vcClaim.identifier = inviteId;
|
||||
}
|
||||
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
const vcPayload: { vc: VerifiableCredentialClaim } = {
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
"@type": "VerifiableCredential",
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
credentialSubject: vcClaim as unknown as ClaimObject, // Type assertion needed due to object being string
|
||||
},
|
||||
};
|
||||
|
||||
// Create a signature using private key of identity
|
||||
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
||||
return vcJwt;
|
||||
@@ -1317,21 +1385,44 @@ export async function register(
|
||||
axios: Axios,
|
||||
contact: Contact,
|
||||
): Promise<{ success?: boolean; error?: string }> {
|
||||
const vcJwt = await createInviteJwt(activeDid, contact);
|
||||
try {
|
||||
const vcJwt = await createInviteJwt(activeDid, contact);
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const resp = await axios.post<{
|
||||
success?: {
|
||||
handleId?: string;
|
||||
embeddedRecordError?: string;
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
}>(url, { jwtEncoded: vcJwt });
|
||||
|
||||
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;
|
||||
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("Registration error:", JSON.stringify(resp.data));
|
||||
return { error: "Got a server error when registering." };
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object") {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorMessage =
|
||||
err.message ||
|
||||
(err.response?.data &&
|
||||
typeof err.response.data === "object" &&
|
||||
"message" in err.response.data
|
||||
? (err.response.data as { message: string }).message
|
||||
: undefined);
|
||||
logger.error("Registration error:", errorMessage || JSON.stringify(err));
|
||||
return { error: errorMessage || "Got a server error when registering." };
|
||||
}
|
||||
return { error: message };
|
||||
} else {
|
||||
logger.error(resp);
|
||||
return { error: "Got a server error when registering." };
|
||||
}
|
||||
}
|
||||
@@ -1357,7 +1448,14 @@ export async function setVisibilityUtil(
|
||||
if (resp.status === 200) {
|
||||
const success = resp.data.success;
|
||||
if (success) {
|
||||
db.contacts.update(contact.did, { seesMe: visibility });
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"UPDATE contacts SET seesMe = ? WHERE did = ?",
|
||||
[visibility, contact.did],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
db.contacts.update(contact.did, { seesMe: visibility });
|
||||
}
|
||||
}
|
||||
return { success };
|
||||
} else {
|
||||
|
||||
176
src/libs/fontawesome.ts
Normal file
176
src/libs/fontawesome.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* @file Font Awesome Icon Library Configuration
|
||||
* @description Centralizes Font Awesome icon imports and library configuration
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCameraRotate,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsis,
|
||||
faEllipsisVertical,
|
||||
faEnvelopeOpenText,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileContract,
|
||||
faFileLines,
|
||||
faFilter,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHouseChimney,
|
||||
faImage,
|
||||
faImagePortrait,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faMinus,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faQrcode,
|
||||
faRightFromBracket,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faThumbtack,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
// Initialize Font Awesome library with all required icons
|
||||
library.add(
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCameraRotate,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsis,
|
||||
faEllipsisVertical,
|
||||
faEnvelopeOpenText,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileContract,
|
||||
faFileLines,
|
||||
faFilter,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHouseChimney,
|
||||
faImage,
|
||||
faImagePortrait,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faMinus,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRotate,
|
||||
faRightFromBracket,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faThumbtack,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
);
|
||||
|
||||
// Export the FontAwesomeIcon component for use in other files
|
||||
export { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
475
src/libs/util.ts
475
src/libs/util.ts
@@ -5,29 +5,46 @@ import { Buffer } from "buffer";
|
||||
import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||
import {
|
||||
DEFAULT_PUSH_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import {
|
||||
accountsDBPromise,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
updateDefaultSettings,
|
||||
} from "../db/index";
|
||||
import { Account } from "../db/tables/accounts";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||
import { deriveAddress, generateSeed, newIdentifier } from "../libs/crypto";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import {
|
||||
containsHiddenDid,
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
deriveAddress,
|
||||
generateSeed,
|
||||
newIdentifier,
|
||||
simpleDecrypt,
|
||||
simpleEncrypt,
|
||||
} from "../libs/crypto";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { containsHiddenDid } from "../libs/endorserServer";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
GiveSummaryRecord,
|
||||
OfferVerifiableCredential,
|
||||
} from "../libs/endorserServer";
|
||||
import { KeyMeta } from "../libs/crypto/vc";
|
||||
KeyMetaWithPrivate,
|
||||
} from "../interfaces/common";
|
||||
import { GiveSummaryRecord } from "../interfaces/records";
|
||||
import { OfferClaim } from "../interfaces/claims";
|
||||
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
||||
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { sha256 } from "ethereum-cryptography/sha256";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
|
||||
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
@@ -66,18 +83,24 @@ export const UNIT_LONG: Record<string, string> = {
|
||||
};
|
||||
/* eslint-enable prettier/prettier */
|
||||
|
||||
const UNIT_CODES: Record<string, Record<string, string>> = {
|
||||
const UNIT_CODES: Record<
|
||||
string,
|
||||
{ name: string; faIcon: string; decimals: number }
|
||||
> = {
|
||||
BTC: {
|
||||
name: "Bitcoin",
|
||||
faIcon: "bitcoin-sign",
|
||||
decimals: 4,
|
||||
},
|
||||
HUR: {
|
||||
name: "hours",
|
||||
faIcon: "clock",
|
||||
decimals: 0,
|
||||
},
|
||||
USD: {
|
||||
name: "US Dollars",
|
||||
faIcon: "dollar",
|
||||
decimals: 2,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,6 +108,13 @@ export function iconForUnitCode(unitCode: string) {
|
||||
return UNIT_CODES[unitCode]?.faIcon || "question";
|
||||
}
|
||||
|
||||
export function formattedAmount(amount: number, unitCode: string) {
|
||||
const unit = UNIT_CODES[unitCode];
|
||||
const amountStr = amount.toFixed(unit?.decimals ?? 4);
|
||||
const unitName = unit?.name || "?";
|
||||
return amountStr + " " + unitName;
|
||||
}
|
||||
|
||||
// from https://stackoverflow.com/a/175787/845494
|
||||
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
|
||||
//
|
||||
@@ -364,16 +394,19 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
||||
* @param veriClaim is expected to have fields: claim and issuer
|
||||
*/
|
||||
export function offerGiverDid(
|
||||
veriClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||
veriClaim: GenericCredWrapper<OfferClaim>,
|
||||
): string | undefined {
|
||||
let giver;
|
||||
if (
|
||||
veriClaim.claim.offeredBy?.identifier &&
|
||||
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
|
||||
) {
|
||||
giver = veriClaim.claim.offeredBy.identifier;
|
||||
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
|
||||
giver = veriClaim.issuer;
|
||||
const innerClaim = veriClaim.claim as OfferClaim;
|
||||
let giver: string | undefined = undefined;
|
||||
|
||||
giver = innerClaim.offeredBy?.identifier;
|
||||
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||
return giver;
|
||||
}
|
||||
|
||||
giver = veriClaim.issuer;
|
||||
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||
return giver;
|
||||
}
|
||||
return giver;
|
||||
}
|
||||
@@ -384,10 +417,12 @@ export function offerGiverDid(
|
||||
*/
|
||||
export const canFulfillOffer = (
|
||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
isRegistered: boolean,
|
||||
) => {
|
||||
return (
|
||||
isRegistered &&
|
||||
veriClaim.claimType === "Offer" &&
|
||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
|
||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferClaim>)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -457,74 +492,236 @@ export function findAllVisibleToDids(
|
||||
*
|
||||
**/
|
||||
|
||||
export interface AccountKeyInfo extends Account, KeyMeta {}
|
||||
export type AccountKeyInfo = Account & KeyMetaWithPrivate;
|
||||
|
||||
export const retrieveAccountCount = async (): Promise<number> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
return await accountsDB.accounts.count();
|
||||
let result = 0;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbResult = await platformService.dbQuery(
|
||||
`SELECT COUNT(*) FROM accounts`,
|
||||
);
|
||||
if (dbResult?.values?.[0]?.[0]) {
|
||||
result = dbResult.values[0][0] as number;
|
||||
}
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
result = await accountsDB.accounts.count();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const retrieveAccountDids = async (): Promise<string[]> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
const allDids = allAccounts.map((acc) => acc.did);
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccounts = await platformService.dbQuery(`SELECT did FROM accounts`);
|
||||
let allDids =
|
||||
databaseUtil
|
||||
.mapQueryResultToValues(dbAccounts)
|
||||
?.map((row) => row[0] as string) || [];
|
||||
if (USE_DEXIE_DB) {
|
||||
// this is the old way
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
allDids = allAccounts.map((acc) => acc.did);
|
||||
}
|
||||
return allDids;
|
||||
};
|
||||
|
||||
// This is provided and recommended when the full key is not necessary so that
|
||||
// future work could separate this info from the sensitive key material.
|
||||
/**
|
||||
* This is provided and recommended when the full key is not necessary so that
|
||||
* future work could separate this info from the sensitive key material.
|
||||
*
|
||||
* If you need the private key data, use retrieveFullyDecryptedAccount instead.
|
||||
*/
|
||||
export const retrieveAccountMetadata = async (
|
||||
activeDid: string,
|
||||
): Promise<AccountKeyInfo | undefined> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
): Promise<Account | undefined> => {
|
||||
let result: Account | undefined = undefined;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccount = await platformService.dbQuery(
|
||||
`SELECT * FROM accounts WHERE did = ?`,
|
||||
[activeDid],
|
||||
);
|
||||
const account = databaseUtil.mapQueryResultToValues(dbAccount)[0] as Account;
|
||||
if (account) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata;
|
||||
result = metadata;
|
||||
} else {
|
||||
return undefined;
|
||||
result = undefined;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
if (account) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
result = metadata;
|
||||
} else {
|
||||
result = undefined;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const array = await accountsDB.accounts.toArray();
|
||||
return array.map((account) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This contains sensitive data. If possible, use retrieveAccountMetadata instead.
|
||||
*
|
||||
* @param activeDid
|
||||
* @returns account info with private key data decrypted
|
||||
*/
|
||||
export const retrieveFullyDecryptedAccount = async (
|
||||
activeDid: string,
|
||||
): Promise<AccountKeyInfo | undefined> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
return account;
|
||||
): Promise<Account | undefined> => {
|
||||
let result: Account | undefined = undefined;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbSecrets = await platformService.dbQuery(
|
||||
`SELECT secretBase64 from secret`,
|
||||
);
|
||||
if (
|
||||
!dbSecrets ||
|
||||
dbSecrets.values.length === 0 ||
|
||||
dbSecrets.values[0].length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
"No secret found. We recommend you clear your data and start over.",
|
||||
);
|
||||
}
|
||||
const secretBase64 = dbSecrets.values[0][0] as string;
|
||||
const secret = base64ToArrayBuffer(secretBase64);
|
||||
const dbAccount = await platformService.dbQuery(
|
||||
`SELECT * FROM accounts WHERE did = ?`,
|
||||
[activeDid],
|
||||
);
|
||||
if (
|
||||
!dbAccount ||
|
||||
dbAccount.values.length === 0 ||
|
||||
dbAccount.values[0].length === 0
|
||||
) {
|
||||
throw new Error("Account not found.");
|
||||
}
|
||||
const fullAccountData = databaseUtil.mapQueryResultToValues(
|
||||
dbAccount,
|
||||
)[0] as AccountEncrypted;
|
||||
const identityEncr = base64ToArrayBuffer(fullAccountData.identityEncrBase64);
|
||||
const mnemonicEncr = base64ToArrayBuffer(fullAccountData.mnemonicEncrBase64);
|
||||
fullAccountData.identity = await simpleDecrypt(identityEncr, secret);
|
||||
fullAccountData.mnemonic = await simpleDecrypt(mnemonicEncr, secret);
|
||||
result = fullAccountData;
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
result = account;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// let's try and eliminate this
|
||||
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
|
||||
Array<AccountKeyInfo>
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<
|
||||
AccountEncrypted[]
|
||||
> => {
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
return allAccounts;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||
let result = accounts.map((account) => {
|
||||
return account as AccountEncrypted;
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const array = await accountsDB.accounts.toArray();
|
||||
result = array.map((account) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
// This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
|
||||
const identityStr = JSON.stringify(identity);
|
||||
const encryptedAccount = {
|
||||
identityEncrBase64: sha256(
|
||||
new TextEncoder().encode(identityStr),
|
||||
).toString(),
|
||||
mnemonicEncrBase64: sha256(
|
||||
new TextEncoder().encode(account.mnemonic),
|
||||
).toString(),
|
||||
...metadata,
|
||||
};
|
||||
return encryptedAccount as AccountEncrypted;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a new identity to both SQL and Dexie databases
|
||||
*/
|
||||
export async function saveNewIdentity(
|
||||
identity: IIdentifier,
|
||||
mnemonic: string,
|
||||
derivationPath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// add to the new sql db
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const secrets = await platformService.dbQuery(
|
||||
`SELECT secretBase64 FROM secret`,
|
||||
);
|
||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||
throw new Error(
|
||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
||||
);
|
||||
}
|
||||
const secretBase64 = secrets.values[0][0] as string;
|
||||
const secret = base64ToArrayBuffer(secretBase64);
|
||||
const identityStr = JSON.stringify(identity);
|
||||
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||
const params = [
|
||||
new Date().toISOString(),
|
||||
derivationPath,
|
||||
identity.did,
|
||||
encryptedIdentityBase64,
|
||||
encryptedMnemonicBase64,
|
||||
identity.keys[0].publicKeyHex,
|
||||
];
|
||||
await platformService.dbExec(sql, params);
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
|
||||
await databaseUtil.insertDidSpecificSettings(identity.did);
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add({
|
||||
dateCreated: new Date().toISOString(),
|
||||
derivationPath: derivationPath,
|
||||
did: identity.did,
|
||||
identity: identityStr,
|
||||
mnemonic: mnemonic,
|
||||
publicKeyHex: identity.keys[0].publicKeyHex,
|
||||
});
|
||||
await updateDefaultSettings({ activeDid: identity.did });
|
||||
await insertDidSpecificSettings(identity.did);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to update default settings:", error);
|
||||
throw new Error(
|
||||
"Failed to set default settings. Please try again or restart the app.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
||||
* @return {Promise<string>} with the DID of the new identity
|
||||
@@ -536,23 +733,14 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
||||
deriveAddress(mnemonic);
|
||||
|
||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||
const identity = JSON.stringify(newId);
|
||||
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add({
|
||||
dateCreated: new Date().toISOString(),
|
||||
derivationPath: derivationPath,
|
||||
did: newId.did,
|
||||
identity: identity,
|
||||
mnemonic: mnemonic,
|
||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||
await saveNewIdentity(newId, mnemonic, derivationPath);
|
||||
await databaseUtil.updateDidSpecificSettings(newId.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
|
||||
await updateDefaultSettings({ activeDid: newId.did });
|
||||
//console.log("Updated default settings in util");
|
||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||
}
|
||||
return newId.did;
|
||||
};
|
||||
|
||||
@@ -570,9 +758,19 @@ export const registerAndSavePasskey = async (
|
||||
passkeyCredIdHex,
|
||||
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
||||
};
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add(account);
|
||||
const insertStatement = databaseUtil.generateInsertStatement(
|
||||
account,
|
||||
"accounts",
|
||||
);
|
||||
await PlatformServiceFactory.getInstance().dbExec(
|
||||
insertStatement.sql,
|
||||
insertStatement.params,
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add(account);
|
||||
}
|
||||
return account;
|
||||
};
|
||||
|
||||
@@ -580,13 +778,22 @@ export const registerSaveAndActivatePasskey = async (
|
||||
keyName: string,
|
||||
): Promise<Account> => {
|
||||
const account = await registerAndSavePasskey(keyName);
|
||||
await updateDefaultSettings({ activeDid: account.did });
|
||||
await updateAccountSettings(account.did, { isRegistered: false });
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
|
||||
await databaseUtil.updateDidSpecificSettings(account.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateDefaultSettings({ activeDid: account.did });
|
||||
await updateAccountSettings(account.did, { isRegistered: false });
|
||||
}
|
||||
return account;
|
||||
};
|
||||
|
||||
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
return (
|
||||
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
||||
60
|
||||
@@ -602,7 +809,10 @@ export const sendTestThroughPushServer = async (
|
||||
subscriptionJSON: PushSubscriptionJSON,
|
||||
skipFilter: boolean,
|
||||
): Promise<AxiosResponse> => {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
@@ -630,3 +840,96 @@ export const sendTestThroughPushServer = async (
|
||||
logger.log("Got response from web push server:", response);
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Contact object to a CSV line string following the established format.
|
||||
* The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
|
||||
* where contactMethods is stored as a stringified JSON array.
|
||||
*
|
||||
* @param contact - The Contact object to convert
|
||||
* @returns A CSV-formatted string representing the contact
|
||||
* @throws {Error} If the contact object is missing required fields
|
||||
*/
|
||||
export const contactToCsvLine = (contact: Contact): string => {
|
||||
if (!contact.did) {
|
||||
throw new Error("Contact must have a did field");
|
||||
}
|
||||
|
||||
// Escape fields that might contain commas or quotes
|
||||
const escapeField = (field: string | boolean | undefined): string => {
|
||||
if (field === undefined) return "";
|
||||
const str = String(field);
|
||||
if (str.includes(",") || str.includes('"')) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
// Handle contactMethods array by stringifying it
|
||||
const contactMethodsStr = contact.contactMethods
|
||||
? escapeField(JSON.stringify(parseJsonField(contact.contactMethods, [])))
|
||||
: "";
|
||||
|
||||
const fields = [
|
||||
escapeField(contact.name),
|
||||
escapeField(contact.did),
|
||||
escapeField(contact.publicKeyBase64),
|
||||
escapeField(contact.seesMe),
|
||||
escapeField(contact.registered),
|
||||
contactMethodsStr,
|
||||
];
|
||||
|
||||
return fields.join(",");
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface for the JSON export format of database tables
|
||||
*/
|
||||
export interface TableExportData {
|
||||
tableName: string;
|
||||
rows: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the complete database export format
|
||||
*/
|
||||
export interface DatabaseExport {
|
||||
data: {
|
||||
data: Array<TableExportData>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of contacts to the standardized database export JSON format.
|
||||
* This format is used for data migration and backup purposes.
|
||||
*
|
||||
* @param contacts - Array of Contact objects to convert
|
||||
* @returns DatabaseExport object in the standardized format
|
||||
*/
|
||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||
// Convert each contact to a plain object and ensure all fields are included
|
||||
const rows = contacts.map((contact) => ({
|
||||
did: contact.did,
|
||||
name: contact.name || null,
|
||||
contactMethods: contact.contactMethods
|
||||
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
||||
: null,
|
||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||
notes: contact.notes || null,
|
||||
profileImageUrl: contact.profileImageUrl || null,
|
||||
publicKeyBase64: contact.publicKeyBase64 || null,
|
||||
seesMe: contact.seesMe || false,
|
||||
registered: contact.registered || false,
|
||||
}));
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
tableName: "contacts",
|
||||
rows,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user