Compare commits

..

24 Commits

Author SHA1 Message Date
9361f68888 Merge pull request 'docs: add tlmgr font packages' (#122) from kentbull/crowd-funder-for-time-pwa:kent/docs-update-tlmgr-packages into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#122
2024-07-19 20:00:06 -04:00
Kent Bull
f1f98417cd docs: add tlmgr font packages 2024-07-19 17:59:54 -06:00
fcef84bc82 rename "docs" directory to "doc" 2024-07-19 14:40:48 -06:00
1172aad318 Merge pull request 'docs: basic pandoc setup' (#118) from kentbull/crowd-funder-for-time-pwa:kb/add-usage-guide into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#118
2024-07-19 12:47:18 -04:00
9b65fb7ef9 remove remaining getIdentity calls & fix QR code for did:peer 2024-07-15 20:47:10 -06:00
f74b399871 reword some things in help 2024-07-15 19:11:12 -06:00
05398b4de7 add BTC donation address 2024-07-15 17:18:22 -06:00
2aedf6c185 move low-level DID-related create & decode into separate folder (#120)
Co-authored-by: Trent Larson <trent@trentlarson.com>
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#120
Co-authored-by: trentlarson <trent@trentlarson.com>
Co-committed-by: trentlarson <trent@trentlarson.com>
2024-07-13 13:24:54 -04:00
bc00eac143 Merge pull request 'Refactor JWT-creation calls through single function' (#119) from passkey-all into master
Reviewed-on: trent_larson/crowd-funder-for-time-pwa#119
2024-07-11 22:32:30 -04:00
925f3e90bb change first page back to prompts without passkey 2024-07-11 19:54:20 -06:00
bc1846a95a consolidate getIdentity & remove dups 2024-07-11 19:43:56 -06:00
674ca1d63c replace remaining didJwt.createJwt calls with one that checks for did:peer 2024-07-11 19:35:17 -06:00
f184fe4d51 linting cleanup 2024-07-09 19:42:55 -06:00
c67ceebc67 change accessToken to take a DID 2024-07-09 19:20:05 -06:00
c200cdbead add expiration inside JWANT & refactor getHeaders to move toward supporting did:peer 2024-07-09 17:56:48 -06:00
2dd6e9b07a make a passkey-generator in start & home pages, and make that the default 2024-07-06 19:12:31 -06:00
33d6b9df96 misc tweaks and linting clean-up 2024-07-06 13:04:15 -06:00
63d0f3c748 misc syntactic & type-checking clean-up 2024-07-06 07:15:46 -06:00
54d14324a1 allow deletion of an identity 2024-07-05 19:37:45 -06:00
05cc5b011d show a loading indicator on the claim-confirmation screen 2024-07-01 17:55:21 -06:00
a3b0993855 fill in the "Load More" links for plan linkages 2024-06-30 20:10:18 -06:00
596454fc3d add section for gives provided by a plan 2024-06-30 20:06:47 -06:00
5e39b91ee5 fix type of the raw claim sent 2024-06-29 13:32:13 -06:00
dffa007a74 add advanced page & flag for editing raw claims, and fix recipient assignment in detail screen 2024-06-29 10:18:56 -06:00
60 changed files with 1650 additions and 1386 deletions

View File

@@ -47,7 +47,7 @@ npm run lint
``` ```
# (Let's replace this with a .env.development or .env.staging file.) # (Let's replace this with a .env.development or .env.staging file.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there. # The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app npm run build TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app PASSKEYS_ENABLED=yep npm run build
``` ```
* Production * Production

View File

@@ -7,45 +7,55 @@ This uses Pandoc and BasicTex (LaTeX) Installed through Homebrew.
### Set Up ### Set Up
```bash ```bash
# See https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x
brew install pandoc brew install pandoc
brew install basictex brew install basictex
pandoc keystore-migration.md -o keystore-migration.pdf
# Setting up LaTex packages # Setting up LaTex packages
# First update tlmgr # First update tlmgr
sudo tlmgr update --self sudo tlmgr update --self
# Then install LaTex packages # Then install LaTex packages
sudo tlmgr install titlesec
sudo tlmgr install framed
sudo tlmgr install threeparttable
sudo tlmgr install wrapfig
sudo tlmgr install multirow
sudo tlmgr install enumitem
sudo tlmgr install bbding sudo tlmgr install bbding
sudo tlmgr install titling # Required for the fancy headers used sudo tlmgr install enumitem
sudo tlmgr install tabu sudo tlmgr install environ
sudo tlmgr install fancyhdr
sudo tlmgr install framed
sudo tlmgr install import
sudo tlmgr install lastpage # Enables Page X of Y
sudo tlmgr install mdframed sudo tlmgr install mdframed
sudo tlmgr install multirow
sudo tlmgr install needspace
sudo tlmgr install ntheorem
sudo tlmgr install tabu
sudo tlmgr install tcolorbox sudo tlmgr install tcolorbox
sudo tlmgr install textpos sudo tlmgr install textpos
sudo tlmgr install import sudo tlmgr install titlesec
sudo tlmgr install varwidth sudo tlmgr install titling # Required for the fancy headers used
sudo tlmgr install needspace sudo tlmgr install threeparttable
sudo tlmgr install tocloft # Required for \tableofcontents generation
sudo tlmgr install ntheorem
sudo tlmgr install environ
sudo tlmgr install trimspaces sudo tlmgr install trimspaces
sudo tlmgr install lastpage # Enables Page X of Y sudo tlmgr install tocloft # Required for \tableofcontents generation
sudo tlmgr install collection-fontsrecommended # And set up fonts sudo tlmgr install varwidth
sudo tlmgr install libertine # The main font the doc uses sudo tlmgr install wrapfig
# Install fonts
sudo tlmgr install cmbright
sudo tlmgr install collection-fontsrecommended # And set up fonts
sudo tlmgr install fira
sudo tlmgr install fontaxes
sudo tlmgr install libertine # The main font the doc uses
sudo tlmgr install opensans
sudo tlmgr install sourceserifpro
``` ```
#### References
The following guide was adapted to this project except that we install with Brew and have a few more packages.
Guide: https://daniel.feldroy.com/posts/setting-up-latex-on-mac-os-x
### Usage ### Usage
Use the `pandoc` command to generate a PDF. Use the `pandoc` command to generate a PDF.

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 463 KiB

After

Width:  |  Height:  |  Size: 463 KiB

View File

@@ -181,6 +181,7 @@
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
> >
Yes Yes
{{ notification.yesText ? ", " + notification.yesText : "" }}
</button> </button>
<button <button
@@ -192,7 +193,7 @@
" "
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2" class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
> >
No No {{ notification.noText ? ", " + notification.noText : "" }}
</button> </button>
<label <label

View File

@@ -287,11 +287,10 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
giverDid, giverDid,
this.receiver?.did as string, this.receiver?.did as string,
description, description,

View File

@@ -223,11 +223,10 @@ export default class OfferDialog extends Vue {
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitOffer( const result = await createAndSubmitOffer(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
description, description,
amount, amount,
unitCode, unitCode,

View File

@@ -126,7 +126,6 @@ import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { getIdentity } from "@/libs/util";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@@ -348,8 +347,7 @@ export default class PhotoDialog extends Vue {
this.blob = (await cropper?.getBlob()) || undefined; this.blob = (await cropper?.getBlob()) || undefined;
} }
const identifier = await getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
const token = await accessToken(identifier);
const headers = { const headers = {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };

View File

@@ -1,12 +1,11 @@
import axios from "axios"; import axios from "axios";
import * as R from "ramda";
import * as THREE from "three"; import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils"; import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js"; import * as TWEEN from "@tweenjs/tween.js";
import { accountsDB, db } from "@/db"; import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { getHeaders } from "@/libs/endorserServer";
const ANIMATION_DURATION_SECS = 10; const ANIMATION_DURATION_SECS = 10;
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/"; const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
@@ -19,17 +18,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || ""; const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer; const apiServer = settings?.apiServer;
await accountsDB.open(); const headers = await getHeaders(activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const headers = {
"Content-Type": "application/json",
};
const identity = JSON.parse(account?.identity || "null");
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction"; const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const resp = await axios.get(url, { headers: headers }); const resp = await axios.get(url, { headers: headers });

View File

@@ -4,6 +4,10 @@
* See also ../libs/veramo/setup.ts * See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
// This is used in titles and verbiage inside the app.
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
APP_NAME = "Time Safari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000", LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
@@ -32,6 +36,9 @@ export const DEFAULT_PUSH_SERVER =
export const IMAGE_TYPE_PROFILE = "profile"; export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
/** /**
* The possible values for "group" and "type" are in App.vue. * The possible values for "group" and "type" are in App.vue.
* From the notiwind package * From the notiwind package
@@ -41,8 +48,10 @@ export interface NotificationIface {
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string; title: string;
text?: string; text?: string;
noText?: string;
onCancel?: (stopAsking: boolean) => Promise<void>; onCancel?: (stopAsking: boolean) => Promise<void>;
onNo?: (stopAsking: boolean) => Promise<void>; onNo?: (stopAsking: boolean) => Promise<void>;
onYes?: () => Promise<void>; onYes?: () => Promise<void>;
promptToStopAsking?: boolean; promptToStopAsking?: boolean;
yesText?: string;
} }

View File

@@ -37,6 +37,7 @@ export type Settings = {
}>; }>;
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server

View File

@@ -3,11 +3,13 @@ import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39"; import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import { HDNode } from "@ethersproject/hdnode"; import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays";
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer"; import {
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup"; import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { decodeEndorserJwt } from "@/libs/crypto/vc";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
@@ -85,79 +87,18 @@ export const generateSeed = (): string => {
/** /**
* Retreive an access token * Retreive an access token
* *
* @param {IIdentifier} identifier
* @return {*} * @return {*}
*/ */
export const accessToken = async (identifier: IIdentifier) => { export const accessToken = async (did?: string) => {
const did: string = identifier.did; if (did) {
const privateKeyHex: string = identifier.keys[0].privateKeyHex as string; const nowEpoch = Math.floor(Date.now() / 1000);
const endEpoch = nowEpoch + 60; // add one minute
const signer = SimpleSigner(privateKeyHex); const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
return createEndorserJwtForDid(did, tokenPayload);
const nowEpoch = Math.floor(Date.now() / 1000); } else {
const endEpoch = nowEpoch + 60; // add one minute return "";
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
const alg = undefined; // defaults to 'ES256K', more standardized but harder to verify vs ES256K-R
const jwt: string = await didJwt.createJWT(tokenPayload, {
alg,
issuer: did,
signer,
});
return jwt;
};
export const sign = async (privateKeyHex: string) => {
const signer = SimpleSigner(privateKeyHex);
return signer;
};
/**
* Copied out of did-jwt since it's deprecated in that library.
*
* The SimpleSigner returns a configured function for signing data.
*
* @example
* const signer = SimpleSigner(import.meta.env.PRIVATE_KEY)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
export function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = (await signer(data)) as string;
return fromJose(signature);
};
}
// from did-jwt/util; see SimpleSigner above
export function fromJose(signature: string): {
r: string;
s: string;
recoveryParam?: number;
} {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
);
} }
const r = bytesToHex(signatureBytes.slice(0, 32)); };
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
// from did-jwt/util; see SimpleSigner above
export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
/** /**
@return results of uportJwtPayload: @return results of uportJwtPayload:
@@ -175,7 +116,7 @@ export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
} }
// JWT format: { header, payload, signature, data } // JWT format: { header, payload, signature, data }
const jwt = didJwt.decodeJWT(jwtText); const jwt = decodeEndorserJwt(jwtText);
return jwt.payload; return jwt.payload;
}; };

View File

@@ -0,0 +1,96 @@
import { Buffer } from "buffer/";
import { decode as cborDecode } from "cbor-x";
import { bytesToMultibase, multibaseToBytes } from "did-jwt";
import { getWebCrypto } from "@/libs/crypto/vc/passkeyHelpers";
export const PEER_DID_PREFIX = "did:peer:";
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
/**
*
*
* similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto
*
* @returns {Promise<boolean>}
*/
export async function verifyPeerSignature(
payloadBytes: Buffer,
issuerDid: string,
signatureBytes: Uint8Array,
): Promise<boolean> {
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
const WebCrypto = await getWebCrypto();
const verifyAlgorithm = {
name: "ECDSA",
hash: { name: "SHA-256" },
};
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
const keyAlgorithm = {
name: "ECDSA",
namedCurve: publicKeyJwk.crv,
};
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
"jwk",
publicKeyJwk,
keyAlgorithm,
false,
["verify"],
);
const verified = await WebCrypto.subtle.verify(
verifyAlgorithm,
publicKeyCryptoKey,
signatureBytes,
payloadBytes,
);
return verified;
}
export function cborToKeys(publicKeyBytes: Uint8Array) {
const jwkObj = cborDecode(publicKeyBytes);
if (
jwkObj[1] != 2 || // kty "EC"
jwkObj[3] != -7 || // alg "ES256"
jwkObj[-1] != 1 || // crv "P-256"
jwkObj[-2].length != 32 || // x
jwkObj[-3].length != 32 // y
) {
throw new Error("Unable to extract key.");
}
const publicKeyJwk = {
alg: "ES256",
crv: "P-256",
kty: "EC",
x: arrayToBase64Url(jwkObj[-2]),
y: arrayToBase64Url(jwkObj[-3]),
};
const publicKeyBuffer = Buffer.concat([
Buffer.from(jwkObj[-2]),
Buffer.from(jwkObj[-3]),
]);
return { publicKeyJwk, publicKeyBuffer };
}
export function toBase64Url(anythingB64: string) {
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export function arrayToBase64Url(anything: Uint8Array) {
return toBase64Url(Buffer.from(anything).toString("base64"));
}
export function peerDidToPublicKeyBytes(did: string) {
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
}
export function createPeerDid(publicKeyBytes: Uint8Array) {
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
const methodSpecificId = bytesToMultibase(
publicKeyBytes,
"base58btc",
"p256-pub",
);
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
}

112
src/libs/crypto/vc/index.ts Normal file
View File

@@ -0,0 +1,112 @@
/**
* Verifiable Credential & DID functions, specifically for EndorserSearch.org tools
*
* The goal is to make this folder similar across projects, then move it to a library.
* Other projects: endorser-ch, image-api
*
*/
import * as didJwt from "did-jwt";
import { JWTDecoded } from "did-jwt/lib/JWT";
import { IIdentifier } from "@veramo/core";
import * as u8a from "uint8arrays";
import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer";
export const ETHR_DID_PREFIX = "did:ethr:";
/**
* 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;
}
/**
* Tell whether a key is from a passkey
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
*/
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
return !!keyMeta?.passkeyCredIdHex;
}
export async function createEndorserJwtForKey(
account: KeyMeta,
payload: object,
) {
if (account?.identity) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identity: IIdentifier = JSON.parse(account.identity!);
const privateKeyHex = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex as string);
return didJwt.createJWT(payload, {
issuer: account.did,
signer: signer,
});
} else if (account?.passkeyCredIdHex) {
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
} else {
throw new Error("No identity data found to sign for DID " + account.did);
}
}
/**
* Copied out of did-jwt since it's deprecated in that library.
*
* The SimpleSigner returns a configured function for signing data.
*
* @example
* const signer = SimpleSigner(import.meta.env.PRIVATE_KEY)
* signer(data, (err, signature) => {
* ...
* })
*
* @param {String} hexPrivateKey a hex encoded private key
* @return {Function} a configured signer function
*/
function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
return async (data) => {
const signature = (await signer(data)) as string;
return fromJose(signature);
};
}
// from did-jwt/util; see SimpleSigner above
function fromJose(signature: string): {
r: string;
s: string;
recoveryParam?: number;
} {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
);
}
const r = bytesToHex(signatureBytes.slice(0, 32));
const s = bytesToHex(signatureBytes.slice(32, 64));
const recoveryParam =
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
return { r, s, recoveryParam };
}
// from did-jwt/util; see SimpleSigner above
function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16");
}
export function decodeEndorserJwt(jwt: string): JWTDecoded {
return didJwt.decodeJWT(jwt);
}

View File

@@ -1,7 +1,5 @@
import asn1 from "asn1-ber";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
import { decode as cborDecode } from "cbor-x"; import { JWTPayload } from "did-jwt";
import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt";
import { DIDResolutionResult } from "did-resolver"; import { DIDResolutionResult } from "did-resolver";
import { sha256 } from "ethereum-cryptography/sha256.js"; import { sha256 } from "ethereum-cryptography/sha256.js";
import { import {
@@ -21,10 +19,15 @@ import {
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types"; } from "@simplewebauthn/types";
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; import { AppString } from "@/constants/app";
import { unwrapEC2Signature } from "@/libs/crypto/vc/passkeyHelpers";
import {
arrayToBase64Url,
cborToKeys,
peerDidToPublicKeyBytes,
verifyPeerSignature,
} from "@/libs/crypto/vc/didPeer";
const PEER_DID_PREFIX = "did:peer:";
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
export interface JWK { export interface JWK {
kty: string; kty: string;
crv: string; crv: string;
@@ -32,20 +35,12 @@ export interface JWK {
y: string; y: string;
} }
function toBase64Url(anythingB64: string) {
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function arrayToBase64Url(anything: Uint8Array) {
return toBase64Url(Buffer.from(anything).toString("base64"));
}
export async function registerCredential(passkeyName?: string) { export async function registerCredential(passkeyName?: string) {
const options: PublicKeyCredentialCreationOptionsJSON = const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({ await generateRegistrationOptions({
rpName: "Time Safari", rpName: AppString.APP_NAME,
rpID: window.location.hostname, rpID: window.location.hostname,
userName: passkeyName || "Time Safari User", userName: passkeyName || AppString.APP_NAME + " User",
// Don't prompt users for additional information about the authenticator // Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX) // (Recommended for smoother UX)
attestationType: "none", attestationType: "none",
@@ -74,7 +69,7 @@ export async function registerCredential(passkeyName?: string) {
const credIdBase64Url = verification.registrationInfo?.credentialID as string; const credIdBase64Url = verification.registrationInfo?.credentialID as string;
if (attResp.rawId !== credIdBase64Url) { if (attResp.rawId !== credIdBase64Url) {
console.log("Warning! The raw ID does not match the credential ID.") console.log("Warning! The raw ID does not match the credential ID.");
} }
const credIdHex = Buffer.from( const credIdHex = Buffer.from(
base64URLStringToArrayBuffer(credIdBase64Url), base64URLStringToArrayBuffer(credIdBase64Url),
@@ -92,21 +87,6 @@ export async function registerCredential(passkeyName?: string) {
}; };
} }
export function createPeerDid(publicKeyBytes: Uint8Array) {
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
const methodSpecificId = bytesToMultibase(
publicKeyBytes,
"base58btc",
"p256-pub",
);
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
}
function peerDidToPublicKeyBytes(did: string) {
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
}
export class PeerSetup { export class PeerSetup {
public authenticatorData?: ArrayBuffer; public authenticatorData?: ArrayBuffer;
public challenge?: Uint8Array; public challenge?: Uint8Array;
@@ -117,13 +97,17 @@ export class PeerSetup {
issuerDid: string, issuerDid: string,
payload: object, payload: object,
credIdHex: string, credIdHex: string,
expMinutes: number = 1,
) { ) {
const credentialId = arrayBufferToBase64URLString( const credentialId = arrayBufferToBase64URLString(
Buffer.from(credIdHex, "hex").buffer, Buffer.from(credIdHex, "hex").buffer,
); );
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = { const fullPayload = {
...payload, ...payload,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
@@ -159,7 +143,8 @@ export class PeerSetup {
const dataInJwt = { const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url, AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
@@ -178,10 +163,14 @@ export class PeerSetup {
issuerDid: string, issuerDid: string,
payload: object, payload: object,
credIdHex: string, credIdHex: string,
expMinutes: number = 1,
) { ) {
const issuedAt = Math.floor(Date.now() / 1000);
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
const fullPayload = { const fullPayload = {
...payload, ...payload,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataToSignString = JSON.stringify(fullPayload); const dataToSignString = JSON.stringify(fullPayload);
@@ -195,12 +184,12 @@ export class PeerSetup {
allowCredentials: [ allowCredentials: [
{ {
id: credentialId, id: credentialId,
type: "public-key", type: "public-key" as const,
}, },
], ],
challenge: this.challenge.buffer, challenge: this.challenge.buffer,
rpID: window.location.hostname, rpID: window.location.hostname,
userVerification: "preferred", userVerification: "preferred" as const,
}, },
}; };
@@ -209,7 +198,7 @@ export class PeerSetup {
this.authenticatorData = credential?.response.authenticatorData; this.authenticatorData = credential?.response.authenticatorData;
const authenticatorDataBase64Url = arrayBufferToBase64URLString( const authenticatorDataBase64Url = arrayBufferToBase64URLString(
this.authenticatorData, this.authenticatorData as ArrayBuffer,
); );
this.clientDataJsonBase64Url = arrayBufferToBase64URLString( this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
@@ -227,7 +216,8 @@ export class PeerSetup {
const dataInJwt = { const dataInJwt = {
AuthenticationDataB64URL: authenticatorDataBase64Url, AuthenticationDataB64URL: authenticatorDataBase64Url,
ClientDataJSONB64URL: this.clientDataJsonBase64Url, ClientDataJSONB64URL: this.clientDataJsonBase64Url,
iat: Math.floor(Date.now() / 1000), exp: expiryTime,
iat: issuedAt,
iss: issuerDid, iss: issuerDid,
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
@@ -237,8 +227,9 @@ export class PeerSetup {
.replace(/\//g, "_") .replace(/\//g, "_")
.replace(/=+$/, ""); .replace(/=+$/, "");
const origSignature = Buffer.from(credential?.response.signature) const origSignature = Buffer.from(credential?.response.signature).toString(
.toString("base64") "base64",
);
this.signature = origSignature this.signature = origSignature
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_") .replace(/\//g, "_")
@@ -248,6 +239,9 @@ export class PeerSetup {
return jwt; return jwt;
} }
// To use this, add the asn1-ber library and add this import:
// import asn1 from "asn1-ber";
//
// return a low-level signing function, similar to createJWS approach // return a low-level signing function, similar to createJWS approach
// async webAuthnES256KSigner(credentialID: string) { // async webAuthnES256KSigner(credentialID: string) {
// return async (data: string | Uint8Array) => { // return async (data: string | Uint8Array) => {
@@ -304,6 +298,16 @@ export class PeerSetup {
// } // }
} }
export async function createDidPeerJwt(
did: string,
credIdHex: string,
payload: object,
): Promise<string> {
const peerSetup = new PeerSetup();
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
return jwt;
}
// I'd love to use this but it doesn't verify. // I'd love to use this but it doesn't verify.
// Requires: // Requires:
// npm install @noble/curves // npm install @noble/curves
@@ -376,6 +380,7 @@ export async function verifyJwtSimplewebauthn(
return verification.verified; return verification.verified;
} }
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto( export async function verifyJwtWebCrypto(
credId: Base64URLString, credId: Base64URLString,
issuerDid: string, issuerDid: string,
@@ -394,35 +399,10 @@ export async function verifyJwtWebCrypto(
// Construct the preimage // Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]); const preimage = Buffer.concat([authDataFromBase, hash]);
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
const WebCrypto = await getWebCrypto();
const verifyAlgorithm = {
name: "ECDSA",
hash: { name: "SHA-256" },
};
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
const keyAlgorithm = {
name: "ECDSA",
namedCurve: publicKeyJwk.crv,
};
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
"jwk",
publicKeyJwk,
keyAlgorithm,
false,
["verify"],
);
const verified = await WebCrypto.subtle.verify(
verifyAlgorithm,
publicKeyCryptoKey,
finalSigBuffer,
preimage,
);
return verified;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
if (!did.startsWith("did:peer:0z")) { if (!did.startsWith("did:peer:0z")) {
throw new Error( throw new Error(
@@ -463,12 +443,15 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
} }
// convert COSE public key to PEM format // convert COSE public key to PEM format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function COSEtoPEM(cose: Buffer) { function COSEtoPEM(cose: Buffer) {
// const alg = cose.get(3); // Algorithm // const alg = cose.get(3); // Algorithm
const x = cose[-2]; // x-coordinate const x = cose[-2]; // x-coordinate
const y = cose[-3]; // y-coordinate const y = cose[-3]; // y-coordinate
// Ensure the coordinates are in the correct format // 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]); const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
// Convert to PEM format // Convert to PEM format
@@ -479,6 +462,7 @@ ${pubKeyBuffer.toString("base64")}
return pem; return pem;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecode(input: string) { function base64urlDecode(input: string) {
input = input.replace(/-/g, "+").replace(/_/g, "/"); input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4); const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
@@ -490,13 +474,14 @@ function base64urlDecode(input: string) {
return bytes.buffer; return bytes.buffer;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncode(buffer: ArrayBuffer) { function base64urlEncode(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer)); const str = String.fromCharCode(...new Uint8Array(buffer));
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
} }
// from @simplewebauthn/browser // from @simplewebauthn/browser
function arrayBufferToBase64URLString(buffer) { function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
let str = ""; let str = "";
for (const charCode of bytes) { for (const charCode of bytes) {
@@ -520,31 +505,7 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
return buffer; return buffer;
} }
function cborToKeys(publicKeyBytes: Uint8Array) { // eslint-disable-next-line @typescript-eslint/no-unused-vars
const jwkObj = cborDecode(publicKeyBytes);
if (
jwkObj[1] != 2 || // kty "EC"
jwkObj[3] != -7 || // alg "ES256"
jwkObj[-1] != 1 || // crv "P-256"
jwkObj[-2].length != 32 || // x
jwkObj[-3].length != 32 // y
) {
throw new Error("Unable to extract key.");
}
const publicKeyJwk = {
alg: "ES256",
crv: "P-256",
kty: "EC",
x: arrayToBase64Url(jwkObj[-2]),
y: arrayToBase64Url(jwkObj[-3]),
};
const publicKeyBuffer = Buffer.concat([
Buffer.from(jwkObj[-2]),
Buffer.from(jwkObj[-3]),
]);
return { publicKeyJwk, publicKeyBuffer };
}
async function pemToCryptoKey(pem: string) { async function pemToCryptoKey(pem: string) {
const binaryDerString = atob( const binaryDerString = atob(
pem pem

View File

@@ -55,8 +55,8 @@ export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
} }
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts // from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
let webCrypto: unknown = undefined; let webCrypto: { subtle: SubtleCrypto } | undefined = undefined;
export function getWebCrypto() { export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
/** /**
* Hello there! If you came here wondering why this method is asynchronous when use of * Hello there! If you came here wondering why this method is asynchronous when use of
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this * `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
@@ -67,25 +67,28 @@ export function getWebCrypto() {
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense * TODO: If it's after February 2025 when you read this then consider whether it still makes sense
* to keep this method asynchronous. * to keep this method asynchronous.
*/ */
const toResolve = new Promise((resolve, reject) => { const toResolve: Promise<{ subtle: SubtleCrypto }> = new Promise(
if (webCrypto) { (resolve, reject) => {
return resolve(webCrypto); if (webCrypto) {
} return resolve(webCrypto);
/** }
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times /**
* support (and Node v20+) * Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
*/ * support (and Node v20+)
const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto(); */
if (_globalThisCrypto) { const _globalThisCrypto =
webCrypto = _globalThisCrypto; _getWebCryptoInternals.stubThisGlobalThisCrypto();
return resolve(webCrypto); if (_globalThisCrypto) {
} webCrypto = _globalThisCrypto;
// We tried to access it both in Node and globally, so bail out return resolve(webCrypto);
return reject(new MissingWebCrypto()); }
}); // We tried to access it both in Node and globally, so bail out
return reject(new MissingWebCrypto());
},
);
return toResolve; return toResolve;
} }
export class MissingWebCrypto extends Error { class MissingWebCrypto extends Error {
constructor() { constructor() {
const message = "An instance of the Crypto API could not be located"; const message = "An instance of the Crypto API could not be located";
super(message); super(message);
@@ -93,10 +96,10 @@ export class MissingWebCrypto extends Error {
} }
} }
// Make it possible to stub return values during testing // Make it possible to stub return values during testing
export const _getWebCryptoInternals = { const _getWebCryptoInternals = {
stubThisGlobalThisCrypto: () => globalThis.crypto, stubThisGlobalThisCrypto: () => globalThis.crypto,
// Make it possible to reset the `webCrypto` at the top of the file // Make it possible to reset the `webCrypto` at the top of the file
setCachedCrypto: (newCrypto: unknown) => { setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {
webCrypto = newCrypto; webCrypto = newCrypto;
}, },
}; };

View File

@@ -1,19 +1,13 @@
import { import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
Axios,
AxiosRequestConfig,
AxiosResponse,
RawAxiosRequestHeaders,
} from "axios";
import * as didJwt from "did-jwt";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index"; import { NonsensitiveDexie } from "@/db/index";
import { getIdentity } from "@/libs/util"; import { getAccount } from "@/libs/util";
import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@@ -453,28 +447,30 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
async function getHeaders(identity: IIdentifier | null) { export async function getHeaders(did?: string) {
const headers: RawAxiosRequestHeaders = { const headers: { "Content-Type": string; Authorization?: string } = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (identity) { if (did) {
const token = await accessToken(identity); const token = await accessToken(did);
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} else {
// it's often OK to request without auth; we assume necessary checks are done earlier
} }
return headers; return headers;
} }
/** /**
* @param handleId nullable, in which case "undefined" will be returned * @param handleId nullable, in which case "undefined" will be returned
* @param identity nullable, in which case no private info will be returned * @param requesterDid optional, in which case no private info will be returned
* @param axios * @param axios
* @param apiServer * @param apiServer
*/ */
export async function getPlanFromCache( export async function getPlanFromCache(
handleId: string | null, handleId: string | null,
identity: IIdentifier | null,
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
requesterDid?: string,
): Promise<PlanSummaryRecord | undefined> { ): Promise<PlanSummaryRecord | undefined> {
if (!handleId) { if (!handleId) {
return undefined; return undefined;
@@ -485,7 +481,7 @@ export async function getPlanFromCache(
apiServer + apiServer +
"/api/v2/report/plans?handleId=" + "/api/v2/report/plans?handleId=" +
encodeURIComponent(handleId); encodeURIComponent(handleId);
const headers = await getHeaders(identity); const headers = await getHeaders(requesterDid);
try { try {
const resp = await axios.get(url, { headers }); const resp = await axios.get(url, { headers });
if (resp.status === 200 && resp.data?.data?.length > 0) { if (resp.status === 200 && resp.data?.data?.length > 0) {
@@ -519,18 +515,9 @@ export async function setPlanInCache(
} }
/** /**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * Construct GiveAction VC for submission to server
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or amount
* @param amount may be null; should have this or description
*/ */
export async function createAndSubmitGive( export function constructGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid?: string | null, fromDid?: string | null,
toDid?: string, toDid?: string,
description?: string, description?: string,
@@ -540,9 +527,9 @@ export async function createAndSubmitGive(
fulfillsOfferHandleId?: string, fulfillsOfferHandleId?: string,
isTrade: boolean = false, isTrade: boolean = false,
imageUrl?: string, imageUrl?: string,
): Promise<CreateAndSubmitClaimResult> { ): GiveVerifiableCredential {
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org", "@context": SCHEMA_ORG_CONTEXT,
"@type": "GiveAction", "@type": "GiveAction",
recipient: toDid ? { identifier: toDid } : undefined, recipient: toDid ? { identifier: toDid } : undefined,
agent: fromDid ? { identifier: fromDid } : undefined, agent: fromDid ? { identifier: fromDid } : undefined,
@@ -569,9 +556,46 @@ export async function createAndSubmitGive(
if (imageUrl) { if (imageUrl) {
vcClaim.image = imageUrl; vcClaim.image = imageUrl;
} }
return vcClaim;
}
/**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or amount
* @param amount may be null; should have this or description
*/
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
issuerDid: string,
fromDid?: string | null,
toDid?: string,
description?: string,
amount?: number,
unitCode?: string,
fulfillsProjectHandleId?: string,
fulfillsOfferHandleId?: string,
isTrade: boolean = false,
imageUrl?: string,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim = constructGive(
fromDid,
toDid,
description,
amount,
unitCode,
fulfillsProjectHandleId,
fulfillsOfferHandleId,
isTrade,
imageUrl,
);
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericCredWrapper,
identity, issuerDid,
apiServer, apiServer,
axios, axios,
); );
@@ -589,7 +613,7 @@ export async function createAndSubmitGive(
export async function createAndSubmitOffer( export async function createAndSubmitOffer(
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
identity: IIdentifier, issuerDid: string,
description?: string, description?: string,
amount?: number, amount?: number,
unitCode?: string, unitCode?: string,
@@ -598,9 +622,9 @@ export async function createAndSubmitOffer(
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
const vcClaim: OfferVerifiableCredential = { const vcClaim: OfferVerifiableCredential = {
"@context": "https://schema.org", "@context": SCHEMA_ORG_CONTEXT,
"@type": "Offer", "@type": "Offer",
offeredBy: { identifier: identity.did }, offeredBy: { identifier: issuerDid },
validThrough: expirationDate || undefined, validThrough: expirationDate || undefined,
}; };
if (amount) { if (amount) {
@@ -624,7 +648,7 @@ export async function createAndSubmitOffer(
} }
return createAndSubmitClaim( return createAndSubmitClaim(
vcClaim as GenericCredWrapper, vcClaim as GenericCredWrapper,
identity, issuerDid,
apiServer, apiServer,
axios, axios,
); );
@@ -632,7 +656,7 @@ export async function createAndSubmitOffer(
// similar logic is found in endorser-mobile // similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async ( export const createAndSubmitConfirmation = async (
identifier: IIdentifier, issuerDid: string,
claim: GenericVerifiableCredential, claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined, handleId: string | undefined,
@@ -645,16 +669,16 @@ export const createAndSubmitConfirmation = async (
), ),
); );
const confirmationClaim: GenericVerifiableCredential = { const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org", "@context": SCHEMA_ORG_CONTEXT,
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,
}; };
return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios); return createAndSubmitClaim(confirmationClaim, issuerDid, apiServer, axios);
}; };
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericVerifiableCredential,
identity: IIdentifier, issuerDid: string,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
): Promise<CreateAndSubmitClaimResult> { ): Promise<CreateAndSubmitClaimResult> {
@@ -667,34 +691,15 @@ export async function createAndSubmitClaim(
}, },
}; };
// Create a signature using private key of identity const vcJwt: string = await createEndorserJwtForDid(issuerDid, vcPayload);
const firstKey = identity.keys[0];
const privateKeyHex = firstKey?.privateKeyHex;
if (!privateKeyHex) {
throw {
error: "No private key",
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const signer = await SimpleSigner(privateKeyHex);
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
issuer: identity.did,
signer,
});
// Make the xhr request payload // Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt }); const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = `${apiServer}/api/v2/claim`; const url = `${apiServer}/api/v2/claim`;
const token = await accessToken(identity);
const response = await axios.post(url, payload, { const response = await axios.post(url, payload, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`,
}, },
}); });
@@ -716,6 +721,14 @@ export async function createAndSubmitClaim(
} }
} }
export async function createEndorserJwtForDid(
issuerDid: string,
payload: object,
) {
const account = await getAccount(issuerDid);
return createEndorserJwtForKey(account as KeyMeta, payload);
}
/** /**
* An AcceptAction is when someone accepts some contract or pledge. * An AcceptAction is when someone accepts some contract or pledge.
* *
@@ -919,18 +932,31 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
}; };
}; };
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 register( export async function register(
activeDid: string, activeDid: string,
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
contact: Contact, contact: Contact,
) { ) {
const identity = await getIdentity(activeDid);
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org", "@context": SCHEMA_ORG_CONTEXT,
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { identifier: identity.did }, agent: { identifier: activeDid },
object: SERVICE_ID, object: SERVICE_ID,
participant: { identifier: contact.did }, participant: { identifier: contact.did },
}; };
@@ -943,26 +969,10 @@ export async function register(
}, },
}; };
// Create a signature using private key of identity // Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) { const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload);
return { error: "Private key not found." };
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim"; const url = apiServer + "/api/v2/claim";
const headers = await getHeaders(identity); const resp = await axios.post(url, { jwtEncoded: vcJwt });
const resp = await axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) { if (resp.data?.success?.handleId) {
return { success: true }; return { success: true };
} else if (resp.data?.success?.embeddedRecordError) { } else if (resp.data?.success?.embeddedRecordError) {
@@ -991,8 +1001,7 @@ export async function setVisibilityUtil(
} }
const url = const url =
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe"); apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
const identity = await getIdentity(activeDid); const headers = await getHeaders(activeDid);
const headers = await getHeaders(identity);
const payload = JSON.stringify({ did: contact.did }); const payload = JSON.stringify({ did: contact.did });
try { try {
@@ -1021,16 +1030,16 @@ export async function setVisibilityUtil(
* *
* @param apiServer endorser server URL string * @param apiServer endorser server URL string
* @param axios Axios instance * @param axios Axios instance
* @param {IIdentifier} identity - The identity object to check rate limits for. * @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
export async function fetchEndorserRateLimits( export async function fetchEndorserRateLimits(
apiServer: string, apiServer: string,
axios: Axios, axios: Axios,
identity: IIdentifier, issuerDid: string,
) { ) {
const url = `${apiServer}/api/report/rateLimits`; const url = `${apiServer}/api/report/rateLimits`;
const headers = await getHeaders(identity); const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig); return await axios.get(url, { headers } as AxiosRequestConfig);
} }
@@ -1039,15 +1048,11 @@ export async function fetchEndorserRateLimits(
* *
* @param apiServer image server URL string * @param apiServer image server URL string
* @param axios Axios instance * @param axios Axios instance
* @param {IIdentifier} identity - The identity object to check rate limits for. * @param {string} issuerDid - The DID for which to check rate limits.
* @returns {Promise<AxiosResponse>} The Axios response object. * @returns {Promise<AxiosResponse>} The Axios response object.
*/ */
export async function fetchImageRateLimits( export async function fetchImageRateLimits(axios: Axios, issuerDid: string) {
apiServer: string,
axios: Axios,
identity: IIdentifier,
) {
const url = DEFAULT_IMAGE_API_SERVER + "/image-limits"; const url = DEFAULT_IMAGE_API_SERVER + "/image-limits";
const headers = await getHeaders(identity); const headers = await getHeaders(issuerDid);
return await axios.get(url, { headers } as AxiosRequestConfig); return await axios.get(url, { headers } as AxiosRequestConfig);
} }

View File

@@ -11,9 +11,14 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
import { Buffer } from "buffer";
import {KeyMeta} from "@/libs/crypto/vc";
import {createPeerDid} from "@/libs/crypto/vc/didPeer";
export const PRIVACY_MESSAGE = export const PRIVACY_MESSAGE =
"The data you send be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to those you allow."; "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
export const UNIT_SHORT: Record<string, string> = { export const UNIT_SHORT: Record<string, string> = {
@@ -193,20 +198,17 @@ export function findAllVisibleToDids(
* *
**/ **/
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => { export interface AccountKeyInfo extends Account, KeyMeta {}
export const getAccount = async (
activeDid: string,
): Promise<AccountKeyInfo | undefined> => {
await accountsDB.open(); await accountsDB.open();
const account = (await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first()) as Account; .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); return account;
if (!identity) {
throw new Error(
`Attempted to load identity ${activeDid} but no identifier was found`,
);
}
return identity;
}; };
/** /**
@@ -239,6 +241,38 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
return newId.did; return newId.did;
}; };
export const registerAndSavePasskey = async (
keyName: string,
): Promise<Account> => {
const cred = await registerCredential(keyName);
const publicKeyBytes = cred.publicKeyBytes;
const did = createPeerDid(publicKeyBytes as Uint8Array);
const passkeyCredIdHex = cred.credIdHex as string;
const account = {
dateCreated: new Date().toISOString(),
did,
passkeyCredIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
};
await accountsDB.open();
await accountsDB.accounts.add(account);
return account;
};
export const registerSaveAndActivatePasskey = async (
keyName: string,
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did,
});
return account;
};
export const sendTestThroughPushServer = async ( export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON, subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean, skipFilter: boolean,

View File

@@ -38,6 +38,11 @@ const routes: Array<RouteRecordRaw> = [
name: "claim", name: "claim",
component: () => import("../views/ClaimView.vue"), component: () => import("../views/ClaimView.vue"),
}, },
{
path: "/claim-add-raw/:id?",
name: "claim-add-raw",
component: () => import("../views/ClaimAddRawView.vue"),
},
{ {
path: "/confirm-contact", path: "/confirm-contact",
name: "confirm-contact", name: "confirm-contact",

View File

@@ -6,6 +6,9 @@ import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
/**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
*/
export async function testServerRegisterUser() { export async function testServerRegisterUser() {
const testUser0Mnem = const testUser0Mnem =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control"; "seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";

View File

@@ -314,7 +314,7 @@
> >
Advanced Advanced
</h3> </h3>
<div v-if="showAdvanced"> <div v-if="showAdvanced || showGeneralAdvanced">
<p class="text-rose-600 mb-8"> <p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedom! you do not expect. But we support your freedom!
@@ -359,6 +359,7 @@
<div class="text-slate-500 text-sm font-bold">Derivation Path</div> <div class="text-slate-500 text-sm font-bold">Derivation Path</div>
<div <div
v-if="derivationPath"
class="text-sm text-slate-500 flex justify-start items-center mb-1" class="text-sm text-slate-500 flex justify-start items-center mb-1"
> >
<code class="truncate">{{ derivationPath }}</code> <code class="truncate">{{ derivationPath }}</code>
@@ -375,6 +376,12 @@
</button> </button>
<span v-show="showDerCopy">Copied</span> <span v-show="showDerCopy">Copied</span>
</div> </div>
<div
v-else
class="text-sm text-slate-500 flex justify-start items-center mb-1"
>
(none)
</div>
</div> </div>
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
@@ -386,6 +393,27 @@
Switch Identifier Switch Identifier
</router-link> </router-link>
<div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database
</h2>
<div class="ml-4 mt-2">
Import
<input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()">
<button
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="confirmSubmitImportFile()"
>
Import Settings & Contacts
<br />
(excluding Identifier Data)
</button>
</div>
</div>
</div>
<label <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4" class="flex items-center justify-between cursor-pointer my-4"
@@ -583,27 +611,6 @@
</div> </div>
</label> </label>
<div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database
</h2>
<div class="ml-4 mt-2">
Import
<input type="file" @change="uploadImportFile" class="ml-2" />
<div v-if="showContactImport()">
<button
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="confirmSubmitImportFile()"
>
Import Settings & Contacts
<br />
(excluding Identifier Data)
</button>
</div>
</div>
</div>
<div class="flex mt-4"> <div class="flex mt-4">
<button> <button>
<router-link <router-link
@@ -614,19 +621,48 @@
</router-link> </router-link>
</button> </button>
</div> </div>
<label
for="toggleShowGeneralAdvanced"
class="flex items-center justify-between cursor-pointer mt-4"
@click="toggleShowGeneralAdvanced"
>
<!-- label -->
<span class="text-slate-500 text-sm font-bold">
Show All General Advanced Functions
</span>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="showGeneralAdvanced"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
/>
</div>
</label>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie"; import Dexie from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import"; import { ImportProgress } from "dexie-export-import/dist/import";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import EntityIcon from "@/components/EntityIcon.vue";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue"; import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
@@ -638,9 +674,9 @@ import {
NotificationIface, NotificationIface,
} from "@/constants/app"; } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core";
import { import {
ErrorResponse, ErrorResponse,
EndorserRateLimits, EndorserRateLimits,
@@ -648,15 +684,7 @@ import {
fetchEndorserRateLimits, fetchEndorserRateLimits,
fetchImageRateLimits, fetchImageRateLimits,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/"; import { getAccount } from "@/libs/util";
import EntityIcon from "@/components/EntityIcon.vue";
interface IAccount {
did: string;
publicKeyHex: string;
privateHex?: string;
derivationPath: string;
}
const inputImportFileNameRef = ref<Blob>(); const inputImportFileNameRef = ref<Blob>();
@@ -677,31 +705,32 @@ export default class AccountViewView extends Vue {
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
endorserLimits: EndorserRateLimits | null = null; endorserLimits: EndorserRateLimits | null = null;
givenName = ""; givenName = "";
hideRegisterPromptOnNewContact = false;
imageLimits: ImageRateLimits | null = null; imageLimits: ImageRateLimits | null = null;
imageServer = "";
isRegistered = false; isRegistered = false;
isSubscribed = false; isSubscribed = false;
limitsMessage = "";
loadingLimits = false;
notificationMaybeChanged = false; notificationMaybeChanged = false;
profileImageUrl?: string; profileImageUrl?: string;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
showLargeIdenticonId?: string; showAdvanced = false;
showLargeIdenticonUrl?: string; showB64Copy = false;
webPushServer = "";
webPushServerInput = "";
limitsMessage = "";
loadingLimits = false;
showContactGives = false; showContactGives = false;
showDidCopy = false; showDidCopy = false;
showDerCopy = false; showDerCopy = false;
showB64Copy = false; showGeneralAdvanced = false;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
showPubCopy = false; showPubCopy = false;
showAdvanced = false;
hideRegisterPromptOnNewContact = false;
showShortcutBvc = false; showShortcutBvc = false;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
webPushServer = "";
webPushServerInput = "";
/** /**
* Async function executed when the component is mounted. * Async function executed when the component is mounted.
@@ -712,18 +741,9 @@ export default class AccountViewView extends Vue {
*/ */
async mounted() { async mounted() {
try { try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
// Initialize component state with values from the database or defaults // Initialize component state with values from the database or defaults
this.initializeState(settings); await this.initializeState();
await this.processIdentity();
// Get and process the identity
const identity = await this.getIdentity(this.activeDid);
if (identity) {
this.processIdentity(identity);
}
const registration = await navigator.serviceWorker.ready; const registration = await navigator.serviceWorker.ready;
this.subscription = await registration.pushManager.getSubscription(); this.subscription = await registration.pushManager.getSubscription();
@@ -742,9 +762,12 @@ export default class AccountViewView extends Vue {
/** /**
* Initializes component state with values from the database or defaults. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database.
*/ */
initializeState(settings: Settings | undefined) { async initializeState() {
await db.open();
const settings: Settings | undefined =
await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = (settings?.activeDid as string) || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = (settings?.apiServer as string) || ""; this.apiServerInput = (settings?.apiServer as string) || "";
@@ -752,10 +775,12 @@ export default class AccountViewView extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.imageServer = (settings?.imageServer as string) || "";
this.profileImageUrl = settings?.profileImageUrl as string; this.profileImageUrl = settings?.profileImageUrl as string;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
!!settings?.hideRegisterPromptOnNewContact; !!settings?.hideRegisterPromptOnNewContact;
this.showGeneralAdvanced = !!settings?.showGeneralAdvanced;
this.showShortcutBvc = !!settings?.showShortcutBvc; this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer; this.warnIfTestServer = !!settings?.warnIfTestServer;
@@ -763,49 +788,6 @@ export default class AccountViewView extends Vue {
this.webPushServerInput = (settings?.webPushServer as string) || ""; this.webPushServerInput = (settings?.webPushServer as string) || "";
} }
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try {
// Open the accounts database
await accountsDB.open();
// Search for the account with the matching DID (decentralized identifier)
const account: { identity?: string } | undefined =
await accountsDB.accounts.where("did").equals(activeDid).first();
// Return parsed identity or null if not found
return JSON.parse((account?.identity as string) || "null");
} catch (error) {
console.error("Failed to find account:", error);
return null;
}
}
/**
* Asynchronously retrieves headers for HTTP requests.
*
* @param {IIdentifier} identity - The identity object for which to generate the headers.
* @returns {Promise<Record<string,string>>} A Promise that resolves to an object containing the headers.
*
* @throws Will throw an error if unable to generate an access token.
*/
public async getHeaders(
identity: IIdentifier,
): Promise<Record<string, string>> {
try {
const token = await accessToken(identity);
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
return headers;
} catch (error) {
console.error("Failed to get headers:", error);
return Promise.reject(error);
}
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds // call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) { doCopyTwoSecRedo(text: string, fn: () => void) {
fn(); fn();
@@ -819,6 +801,11 @@ export default class AccountViewView extends Vue {
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
toggleShowGeneralAdvanced() {
this.showGeneralAdvanced = !this.showGeneralAdvanced;
this.updateShowGeneralAdvanced();
}
toggleProdWarning() { toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer; this.warnIfProdServer = !this.warnIfProdServer;
this.updateWarnIfProdServer(this.warnIfProdServer); this.updateWarnIfProdServer(this.warnIfProdServer);
@@ -840,25 +827,19 @@ export default class AccountViewView extends Vue {
/** /**
* Processes the identity and updates the component's state. * Processes the identity and updates the component's state.
* @param {IdentityType} identity - Object containing identity information.
*/ */
processIdentity(identity: IIdentifier) { async processIdentity() {
if ( const account: Account | undefined = await getAccount(this.activeDid);
identity && if (account?.identity) {
identity.keys && const identity = JSON.parse(account.identity as string) as IIdentifier;
identity.keys.length > 0 &&
identity.keys[0].meta
) {
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta?.derivationPath as string; this.derivationPath = identity.keys[0].meta?.derivationPath as string;
this.checkLimitsFor(this.activeDid);
db.settings.update(MASTER_SETTINGS_KEY, { } else if (account?.publicKeyHex) {
activeDid: identity.did, this.publicHex = account.publicKeyHex as string;
}); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.checkLimitsFor(identity); this.checkLimitsFor(this.activeDid);
} else {
// Handle the case where any of these are null or undefined
} }
} }
@@ -915,7 +896,7 @@ export default class AccountViewView extends Vue {
public async updateShowContactAmounts() { public async updateShowContactAmounts() {
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives, showContactGivesInline: this.showContactGives,
}); });
} catch (err) { } catch (err) {
@@ -935,10 +916,33 @@ export default class AccountViewView extends Vue {
} }
} }
public async updateShowGeneralAdvanced() {
try {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Advanced Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after general-advanced setting update because:",
err,
);
}
}
public async updateWarnIfProdServer(newSetting: boolean) { public async updateWarnIfProdServer(newSetting: boolean) {
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: newSetting, warnIfProdServer: newSetting,
}); });
} catch (err) { } catch (err) {
@@ -961,7 +965,7 @@ export default class AccountViewView extends Vue {
public async updateWarnIfTestServer(newSetting: boolean) { public async updateWarnIfTestServer(newSetting: boolean) {
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: newSetting, warnIfTestServer: newSetting,
}); });
} catch (err) { } catch (err) {
@@ -985,7 +989,7 @@ export default class AccountViewView extends Vue {
const newSetting = !this.hideRegisterPromptOnNewContact; const newSetting = !this.hideRegisterPromptOnNewContact;
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: newSetting, hideRegisterPromptOnNewContact: newSetting,
}); });
this.hideRegisterPromptOnNewContact = newSetting; this.hideRegisterPromptOnNewContact = newSetting;
@@ -1006,7 +1010,7 @@ export default class AccountViewView extends Vue {
public async updateShowShortcutBvc(newSetting: boolean) { public async updateShowShortcutBvc(newSetting: boolean) {
try { try {
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting, showShortcutBvc: newSetting,
}); });
} catch (err) { } catch (err) {
@@ -1187,9 +1191,8 @@ export default class AccountViewView extends Vue {
} }
async checkLimits() { async checkLimits() {
const identity = await this.getIdentity(this.activeDid); if (this.activeDid) {
if (identity) { this.checkLimitsFor(this.activeDid);
this.checkLimitsFor(identity);
} else { } else {
this.limitsMessage = this.limitsMessage =
"You have no identifier, or your data has been corrupted."; "You have no identifier, or your data has been corrupted.";
@@ -1201,7 +1204,7 @@ export default class AccountViewView extends Vue {
* *
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`. * Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
*/ */
public async checkLimitsFor(identity: IIdentifier) { public async checkLimitsFor(did: string) {
this.loadingLimits = true; this.loadingLimits = true;
this.limitsMessage = ""; this.limitsMessage = "";
@@ -1209,7 +1212,7 @@ export default class AccountViewView extends Vue {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
this.apiServer, this.apiServer,
this.axios, this.axios,
identity, did,
); );
if (resp.status === 200) { if (resp.status === 200) {
this.endorserLimits = resp.data; this.endorserLimits = resp.data;
@@ -1234,11 +1237,7 @@ export default class AccountViewView extends Vue {
); );
} }
} }
const imageResp = await fetchImageRateLimits( const imageResp = await fetchImageRateLimits(this.axios, did);
this.apiServer,
this.axios,
identity,
);
if (imageResp.status === 200) { if (imageResp.status === 200) {
this.imageLimits = imageResp.data; this.imageLimits = imageResp.data;
} }
@@ -1335,9 +1334,9 @@ export default class AccountViewView extends Vue {
* *
* @param {AccountType} account - The account object. * @param {AccountType} account - The account object.
*/ */
private updateActiveAccountProperties(account: IAccount) { private updateActiveAccountProperties(account: Account) {
this.activeDid = account.did; this.activeDid = account.did;
this.derivationPath = account.derivationPath; this.derivationPath = account.derivationPath || "";
this.publicHex = account.publicKeyHex; this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
} }
@@ -1408,11 +1407,7 @@ export default class AccountViewView extends Vue {
return; return;
} }
try { try {
const identity = await this.getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
if (!identity) {
throw Error("No identity found.");
}
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +

View File

@@ -0,0 +1,101 @@
<template>
<QuickNav />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<fa icon="chevron-left" class="fa-fw" />
</button>
Raw Claim
</h1>
</div>
<div class="flex">
<textarea rows="20" class="border-2 w-full" v-model="claimStr"></textarea>
</div>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="submitClaim()"
>
Sign &amp; Send
</button>
</section>
</template>
<script lang="ts">
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import * as serverUtil from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts";
@Component({
components: { GiftedDialog, QuickNav },
})
export default class ClaimAddRawView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = "";
apiServer = "";
claimStr = "";
async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.claimStr = this.$route.query.claim;
try {
this.veriClaim = JSON.parse(this.claimStr);
this.claimStr = JSON.stringify(this.veriClaim, null, 2);
} catch (e) {
// ignore a parse
}
}
async submitClaim() {
const fullClaim = JSON.parse(this.claimStr);
const result = await serverUtil.createAndSubmitClaim(
fullClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Claim submitted.",
},
5000,
);
} else {
console.error("Got error submitting the claim:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the claim. See logs for more info.",
},
-1,
);
}
}
}
</script>

View File

@@ -407,7 +407,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@@ -419,7 +419,6 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -432,7 +431,6 @@ import { GiverReceiverInputInfo } from "@/libs/endorserServer";
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -485,15 +483,12 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray(); const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
this.accountIdentityStr = (account?.identity as string) || "null";
const identity = JSON.parse(this.accountIdentityStr);
const pathParam = window.location.pathname.substring("/claim/".length); const pathParam = window.location.pathname.substring("/claim/".length);
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, identity); await this.loadClaim(claimId, this.activeDid);
} else { } else {
this.$notify( this.$notify(
{ {
@@ -527,33 +522,6 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string) { didInfo(did: string) {
return serverUtil.didInfo( return serverUtil.didInfo(
@@ -564,12 +532,12 @@ export default class ClaimView extends Vue {
); );
} }
async loadClaim(claimId: string, identity: IIdentifier) { async loadClaim(claimId: string, userDid: string) {
const urlPath = libsUtil.isGlobalUri(claimId) const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/" ? "/api/claim/byHandle/"
: "/api/claim/"; : "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId); const url = this.apiServer + urlPath + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity); const headers = await serverUtil.getHeaders(userDid);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -601,7 +569,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/gives?handleId=" + "/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await this.getHeaders(identity); const giveHeaders = await serverUtil.getHeaders(userDid);
const giveResp = await this.axios.get(giveUrl, { const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders, headers: giveHeaders,
}); });
@@ -615,7 +583,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/offers?handleId=" + "/api/v2/report/offers?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const offerHeaders = await this.getHeaders(identity); const offerHeaders = await serverUtil.getHeaders(userDid);
const offerResp = await this.axios.get(offerUrl, { const offerResp = await this.axios.get(offerUrl, {
headers: offerHeaders, headers: offerHeaders,
}); });
@@ -631,12 +599,14 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity); const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
if (response.status === 200) { if (response.status === 200) {
const resultList1 = response.data.result || []; const resultList1 = response.data.result || [];
//const publicUrls = resultList.publicUrls || [];
delete resultList1.publicUrls;
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
const resultList3 = R.reject( const resultList3 = R.reject(
(did: string) => did === this.veriClaim.issuer, (did: string) => did === this.veriClaim.issuer,
@@ -671,15 +641,9 @@ export default class ClaimView extends Vue {
} }
async showFullClaim(claimId: string) { async showFullClaim(claimId: string) {
await accountsDB.open();
const accounts = accountsDB.accounts;
const accountsArr: Account[] = await accounts?.toArray();
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
const url = const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId); this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
const headers = await this.getHeaders(identity); const headers = await serverUtil.getHeaders(this.activeDid);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -758,7 +722,7 @@ export default class ClaimView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
@@ -792,7 +756,7 @@ export default class ClaimView extends Vue {
}; };
this.$router.push(route).then(async () => { this.$router.push(route).then(async () => {
this.resetThisValues(); this.resetThisValues();
await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr)); await this.loadClaim(claimId, this.activeDid);
}); });
} }

View File

@@ -27,7 +27,7 @@
</h1> </h1>
</div> </div>
<div v-if="giveDetails"> <div v-if="giveDetails && !isLoading">
<div class="flex justify-center"> <div class="flex justify-center">
<button <button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@@ -371,9 +371,9 @@
> >
</div> </div>
</div> </div>
<div v-else>This does not have details to confirm.</div> <div v-else-if="!isLoading">This does not have details to confirm.</div>
<div class="mt-4"> <div class="mt-4" v-if="!isLoading">
<a <a
@click="showClaimPage(veriClaim.id)" @click="showClaimPage(veriClaim.id)"
class="text-blue-500 cursor-pointer" class="text-blue-500 cursor-pointer"
@@ -382,11 +382,18 @@
All Generic Info All Generic Info
</a> </a>
</div> </div>
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@@ -400,7 +407,6 @@ import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer"; import { displayAmount, GiverReceiverInputInfo } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
@@ -413,7 +419,6 @@ import { isGiveAction } from "@/libs/util";
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
accountIdentityStr: string = "null";
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -426,6 +431,7 @@ export default class ClaimView extends Vue {
giveDetails = null; giveDetails = null;
giverName = ""; giverName = "";
issuerName = ""; issuerName = "";
isLoading = false;
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
recipientName = ""; recipientName = "";
showDetails = false; showDetails = false;
@@ -452,6 +458,7 @@ export default class ClaimView extends Vue {
} }
async mounted() { async mounted() {
this.isLoading = true;
await db.open(); await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
@@ -462,9 +469,6 @@ export default class ClaimView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Array<Account> = await accounts?.toArray(); const accountsArr: Array<Account> = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
this.accountIdentityStr = (account?.identity as string) || "null";
const identity = JSON.parse(this.accountIdentityStr);
const pathParam = window.location.pathname.substring( const pathParam = window.location.pathname.substring(
"/confirm-gift/".length, "/confirm-gift/".length,
@@ -472,7 +476,7 @@ export default class ClaimView extends Vue {
let claimId; let claimId;
if (pathParam) { if (pathParam) {
claimId = decodeURIComponent(pathParam); claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, identity); await this.loadClaim(claimId, this.activeDid);
} else { } else {
this.$notify( this.$notify(
{ {
@@ -488,6 +492,8 @@ export default class ClaimView extends Vue {
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare // When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
// then use this truer check: navigator.canShare && navigator.canShare() // then use this truer check: navigator.canShare && navigator.canShare()
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
this.isLoading = false;
} }
// insert a space before any capital letters except the initial letter // insert a space before any capital letters except the initial letter
@@ -519,33 +525,6 @@ export default class ClaimView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
return headers;
}
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did: string | undefined) { didInfo(did: string | undefined) {
return serverUtil.didInfo( return serverUtil.didInfo(
@@ -556,14 +535,14 @@ export default class ClaimView extends Vue {
); );
} }
async loadClaim(claimId: string, identity: IIdentifier) { async loadClaim(claimId: string, userDid: string) {
const urlPath = libsUtil.isGlobalUri(claimId) const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/" ? "/api/claim/byHandle/"
: "/api/claim/"; : "/api/claim/";
const url = this.apiServer + urlPath + encodeURIComponent(claimId); const url = this.apiServer + urlPath + encodeURIComponent(claimId);
try { try {
const headers = await this.getHeaders(identity); const headers = await serverUtil.getHeaders(userDid);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
// resp.data is: // resp.data is:
// - a Jwt from https://api.endorser.ch/api-docs/ // - a Jwt from https://api.endorser.ch/api-docs/
@@ -603,7 +582,7 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/v2/report/gives?handleId=" + "/api/v2/report/gives?handleId=" +
encodeURIComponent(this.veriClaim.handleId as string); encodeURIComponent(this.veriClaim.handleId as string);
const giveHeaders = await this.getHeaders(identity); const giveHeaders = await serverUtil.getHeaders(userDid);
const giveResp = await this.axios.get(giveUrl, { const giveResp = await this.axios.get(giveUrl, {
headers: giveHeaders, headers: giveHeaders,
}); });
@@ -674,12 +653,14 @@ export default class ClaimView extends Vue {
this.apiServer + this.apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
const confirmHeaders = await this.getHeaders(identity); const confirmHeaders = await serverUtil.getHeaders(userDid);
const response = await this.axios.get(confirmUrl, { const response = await this.axios.get(confirmUrl, {
headers: confirmHeaders, headers: confirmHeaders,
}); });
if (response.status === 200) { if (response.status === 200) {
const resultList1 = response.data.result || []; const resultList1 = response.data.result || [];
//const publicUrls = resultList.publicUrls || [];
delete resultList1.publicUrls;
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
const resultList3 = R.reject( const resultList3 = R.reject(
(did: string) => did === this.giveDetails.agentDid, (did: string) => did === this.giveDetails.agentDid,
@@ -747,7 +728,7 @@ export default class ClaimView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
@@ -781,7 +762,7 @@ export default class ClaimView extends Vue {
}; };
this.$router.push(route).then(async () => { this.$router.push(route).then(async () => {
this.resetThisValues(); this.resetThisValues();
await this.loadClaim(claimId, JSON.parse(this.accountIdentityStr)); await this.loadClaim(claimId, this.activeDid);
}); });
} }

View File

@@ -106,9 +106,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -116,10 +114,12 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
AgreeVerifiableCredential, AgreeVerifiableCredential,
createEndorserJwtVcFromClaim,
displayAmount, displayAmount,
getHeaders,
GiveSummaryRecord, GiveSummaryRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT, SCHEMA_ORG_CONTEXT,
@@ -142,31 +142,6 @@ export default class ContactAmountssView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() { async created() {
try { try {
await db.open(); await db.open();
@@ -174,8 +149,8 @@ export default class ContactAmountssView extends Vue {
this.contact = (await db.contacts.get(contactDid)) || null; this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = (settings?.apiServer as string) || "";
if (this.activeDid && this.contact) { if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
@@ -199,15 +174,14 @@ export default class ContactAmountssView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid);
let result: Array<GiveSummaryRecord> = []; let result: Array<GiveSummaryRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did) + encodeURIComponent(this.activeDid) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const headers = await this.getHeaders(identity); const headers = await getHeaders(activeDid);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
result = resp.data.data; result = resp.data.data;
@@ -233,8 +207,8 @@ export default class ContactAmountssView extends Vue {
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) + encodeURIComponent(contact.did) +
"&recipientDid=" + "&recipientDid=" +
encodeURIComponent(identity.did); encodeURIComponent(this.activeDid);
const headers2 = await this.getHeaders(identity); const headers2 = await getHeaders(activeDid);
const resp2 = await this.axios.get(url2, { headers: headers2 }); const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) { if (resp2.status === 200) {
result = R.concat(result, resp2.data.data); result = R.concat(result, resp2.data.data);
@@ -289,66 +263,48 @@ export default class ContactAmountssView extends Vue {
object: origClaim, object: origClaim,
}; };
// Make a payload for the claim const vcJwt: string = await createEndorserJwtVcFromClaim(
const vcPayload = { this.activeDid,
vc: { vcClaim,
"@context": ["https://www.w3.org/2018/credentials/v1"], );
type: ["VerifiableCredential"],
credentialSubject: vcClaim, // Make the xhr request payload
}, const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(this.activeDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
}; };
// Create a signature using private key of identity try {
const identity = await this.getIdentity(this.activeDid); const resp = await this.axios.post(url, payload, { headers });
if (identity.keys[0].privateKeyHex !== null) { if (resp.data?.success) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion record.amountConfirmed =
const privateKeyHex: string = identity.keys[0].privateKeyHex!; (origClaim.object?.amountOfThisGood as number) || 1;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success) {
record.amountConfirmed = origClaim.object?.amountOfThisGood || 1;
}
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: userMessage,
},
-1,
);
} }
} catch (error) {
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: userMessage,
},
-1,
);
} }
} }

View File

@@ -72,17 +72,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { IIdentifier } from "@veramo/core";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account, AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { GiverReceiverInputInfo } from "@/libs/endorserServer"; import { GiverReceiverInputInfo } from "@/libs/endorserServer";
@Component({ @Component({
@@ -134,32 +132,7 @@ export default class ContactGiftingView extends Vue {
} }
} }
public async getIdentity(activeDid: string) { openDialog(giver?: GiverReceiverInputInfo) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
openDialog(giver: GiverReceiverInputInfo) {
const recipient = this.projectId const recipient = this.projectId
? undefined ? undefined
: { did: this.activeDid, name: "you" }; : { did: this.activeDid, name: "you" };

View File

@@ -24,6 +24,7 @@
> >
<span class="text-red">Beware!</span> <span class="text-red">Beware!</span>
You aren't sharing your name, so quickly You aren't sharing your name, so quickly
<br />
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
@@ -33,7 +34,11 @@
</p> </p>
</div> </div>
<div @click="onCopyToClipboard()" v-if="activeDid" class="text-center"> <div
@click="onCopyUrlToClipboard()"
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
class="text-center"
>
<!-- <!--
Play with display options: https://qr-code-styling.com/ Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3 See docs: https://www.npmjs.com/package/qr-code-generator-vue3
@@ -44,8 +49,18 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<span> Click that QR to copy your contact URL to your clipboard. </span> <span>
<div>Not scanning? Show it in pieces.</div> Click this or QR code to copy your contact URL to your clipboard.
</span>
</div>
<div v-else-if="activeDid" class="text-center">
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
<span @click="onCopyDidToClipboard()" class="text-blue-500">
Click here to copy your DID to your clipboard.
</span>
<span>
Then give it to them so they can paste it in their list of People.
</span>
</div> </div>
<div class="text-center" v-else> <div class="text-center" v-else>
You have no identitifiers yet, so You have no identitifiers yet, so
@@ -72,7 +87,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import { Buffer } from "buffer/";
import { sha256 } from "ethereum-cryptography/sha256.js"; import { sha256 } from "ethereum-cryptography/sha256.js";
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import * as R from "ramda"; import * as R from "ramda";
@@ -83,24 +98,22 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { import {
deriveAddress, deriveAddress,
getContactPayloadFromJwtUrl, getContactPayloadFromJwtUrl,
nextDerivationPath, nextDerivationPath,
SimpleSigner,
} from "@/libs/crypto"; } from "@/libs/crypto";
import { import {
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
createEndorserJwtForDid,
ENDORSER_JWT_URL_LOCATION, ENDORSER_JWT_URL_LOCATION,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
import { Buffer } from "buffer/";
@Component({ @Component({
components: { components: {
@@ -119,6 +132,8 @@ export default class ContactQRScanShow extends Vue {
isRegistered = false; isRegistered = false;
qrValue = ""; qrValue = "";
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -133,17 +148,9 @@ export default class ContactQRScanShow extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === this.activeDid, accounts);
if (account) { if (account) {
const identity = await this.getIdentity(this.activeDid); const publicKeyHex = account.publicKeyHex;
const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const newDerivPath = nextDerivationPath(account.derivationPath);
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
Buffer.from(nextPublicEncKeyHash).toString("base64");
const contactInfo = { const contactInfo = {
iat: Date.now(), iat: Date.now(),
iss: this.activeDid, iss: this.activeDid,
@@ -152,21 +159,28 @@ export default class ContactQRScanShow extends Vue {
(settings?.firstName || "") + (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey, publicEncKey,
nextPublicEncKeyHash: nextPublicEncKeyHashBase64,
profileImageUrl: settings?.profileImageUrl, profileImageUrl: settings?.profileImageUrl,
registered: settings?.isRegistered, registered: settings?.isRegistered,
}, },
}; };
const alg = undefined; if (account?.mnemonic && account?.derivationPath) {
const privateKeyHex: string = identity.keys[0].privateKeyHex; const newDerivPath = nextDerivationPath(
const signer = await SimpleSigner(privateKeyHex); account.derivationPath as string,
// create a JWT for the request );
const vcJwt: string = await didJwt.createJWT(contactInfo, { const nextPublicHex = deriveAddress(
alg: alg, account.mnemonic as string,
issuer: identity.did, newDerivPath,
signer: signer, )[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(this.activeDid, contactInfo);
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION;
this.qrValue = viewPrefix + vcJwt; this.qrValue = viewPrefix + vcJwt;
} }
@@ -184,23 +198,6 @@ export default class ContactQRScanShow extends Vue {
); );
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find(
(acc) => acc.did === activeDid,
accounts,
);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to show contact info with no identifier available.",
);
}
return identity;
}
/** /**
* *
* @param content is the result of a QR scan, an array with one item with a rawValue property * @param content is the result of a QR scan, an array with one item with a rawValue property
@@ -433,7 +430,7 @@ export default class ContactQRScanShow extends Vue {
); );
} }
onCopyToClipboard() { onCopyUrlToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
useClipboard() useClipboard()
.copy(this.qrValue) .copy(this.qrValue)
@@ -450,5 +447,22 @@ export default class ContactQRScanShow extends Vue {
); );
}); });
} }
onCopyDidToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
useClipboard()
.copy(this.activeDid)
.then(() => {
this.$notify(
{
group: "alert",
type: "info",
title: "Copied",
text: "Your DID was copied to the clipboard. Have them paste it on their 'People' screen to add you.",
},
10000,
);
});
}
} }
</script> </script>

View File

@@ -303,20 +303,20 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { IndexableType } from "dexie"; import { IndexableType } from "dexie";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken, getContactPayloadFromJwtUrl } from "@/libs/crypto"; import { getContactPayloadFromJwtUrl } from "@/libs/crypto";
import { import {
CONTACT_CSV_HEADER, CONTACT_CSV_HEADER,
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
getHeaders,
isDid, isDid,
register, register,
setVisibilityUtil, setVisibilityUtil,
@@ -326,7 +326,6 @@ import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import { Account } from "@/db/tables/accounts";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
@@ -400,36 +399,6 @@ export default class ContactsView extends Vue {
); );
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
public async getHeadersAndIdentity(activeDid: string) {
const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity);
return { headers, identity };
}
async loadGives() { async loadGives() {
if (!this.activeDid) { if (!this.activeDid) {
return; return;
@@ -481,7 +450,7 @@ export default class ContactsView extends Vue {
}; };
try { try {
const { headers } = await this.getHeadersAndIdentity(this.activeDid); const headers = await getHeaders(this.activeDid);
const givenByUrl = const givenByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@@ -954,8 +923,19 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const identity = await this.getIdentity(this.activeDid); const headers = await getHeaders(this.activeDid);
const headers = await this.getHeaders(identity); if (!headers["Authorization"]) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Identity",
text: "There is no identity to use to check visibility.",
},
3000,
);
return;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });

View File

@@ -136,11 +136,11 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
capitalizeAndInsertSpacesBeforeCaps, capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact, didInfoForContact,
displayAmount, displayAmount,
getHeaders,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
GiveVerifiableCredential, GiveVerifiableCredential,
@@ -203,30 +203,6 @@ export default class DIDView extends Vue {
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
} }
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
/** /**
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
@@ -255,7 +231,7 @@ export default class DIDView extends Vue {
this.apiServer + "/api/v2/report/claims?" + queryParams + postfix, this.apiServer + "/api/v2/report/claims?" + queryParams + postfix,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );

View File

@@ -138,8 +138,7 @@ import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { didInfo, getHeaders, PlanData } from "@/libs/endorserServer";
import { didInfo, PlanData } from "@/libs/endorserServer";
@Component({ @Component({
components: { components: {
@@ -203,30 +202,6 @@ export default class DiscoverView extends Vue {
} }
} }
public async buildHeaders(): Promise<HeadersInit> {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async searchAll(beforeId?: string) { public async searchAll(beforeId?: string) {
this.resetCounts(); this.resetCounts();
@@ -247,7 +222,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plans?" + queryParams, this.apiServer + "/api/v2/report/plans?" + queryParams,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );
@@ -337,7 +312,7 @@ export default class DiscoverView extends Vue {
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );

View File

@@ -21,8 +21,17 @@
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1> <h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
<span>From {{ giverName || "somebody not named" }}</span> <span>From {{ giverName }}</span>
<span> to {{ recipientName || "somebody not named" }}</span> <span>
to
{{
givenToProject
? projectName
: givenToRecipient
? recipientName
: "someone unidentified"
}}</span
>
</h1> </h1>
<textarea <textarea
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
@@ -78,7 +87,7 @@
<div class="h-7 mt-4 flex"> <div class="h-7 mt-4 flex">
<input <input
v-if="projectId && !givenToUser" v-if="projectId && !givenToRecipient"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="h-6 w-6 mr-2"
v-model="givenToProject" v-model="givenToProject"
@@ -100,20 +109,24 @@
<div class="h-7 mt-4 flex"> <div class="h-7 mt-4 flex">
<input <input
v-if="!givenToProject" v-if="recipientDid && !givenToProject"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="h-6 w-6 mr-2"
v-model="givenToUser" v-model="givenToRecipient"
/> />
<fa <fa
v-else v-else
icon="square" icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded" class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@click=" @click="notifyUserOfRecipient()"
notifyUser('You cannot assign this both a project and also to you.')
"
/> />
<label class="text-sm mt-1">This was given to you</label> <label class="text-sm mt-1">
{{
recipientDid
? "This was given to " + recipientName
: "No recipient was chosen."
}}
</label>
</div> </div>
<div class="mt-4 flex"> <div class="mt-4 flex">
@@ -121,6 +134,20 @@
<label class="text-sm mt-1">This was a trade (not a gift)</label> <label class="text-sm mt-1">This was a trade (not a gift)</label>
</div> </div>
<div class="mt-4 flex">
<router-link
:to="{
name: 'claim-add-raw',
query: {
claim: constructGiveParam(),
},
}"
class="text-blue-500"
>
Edit & Submit Raw
</router-link>
</div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
<fa <fa
@@ -153,11 +180,17 @@ import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { createAndSubmitGive, getPlanFromCache } from "@/libs/endorserServer"; import {
constructGive,
createAndSubmitGive,
didInfo,
getPlanFromCache,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { Contact } from "@/db/tables/contacts";
@Component({ @Component({
components: { components: {
@@ -176,7 +209,7 @@ export default class GiftedDetails extends Vue {
description = ""; description = "";
destinationNameAfter = ""; destinationNameAfter = "";
givenToProject = false; givenToProject = false;
givenToUser = false; givenToRecipient = false;
giverDid: string | undefined; giverDid: string | undefined;
giverName = ""; giverName = "";
hideBackButton = false; hideBackButton = false;
@@ -188,7 +221,6 @@ export default class GiftedDetails extends Vue {
projectName = "a project"; projectName = "a project";
recipientDid = ""; recipientDid = "";
recipientName = ""; recipientName = "";
showGivenToUser = false;
unitCode = "HUR"; unitCode = "HUR";
libsUtil = libsUtil; libsUtil = libsUtil;
@@ -234,18 +266,36 @@ export default class GiftedDetails extends Vue {
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
if (this.giverDid && !this.giverName) { let allContacts: Contact[] = [];
this.giverName = let allMyDids: string[] = [];
this.giverDid === this.activeDid ? "you" : "someone not named"; if (
} (this.giverDid && !this.giverName) ||
this.givenToUser = this.recipientDid === this.activeDid; (this.recipientDid && !this.recipientName)
if (this.recipientDid && !this.recipientName) { ) {
this.recipientName = allContacts = await db.contacts.toArray();
this.recipientDid === this.activeDid ? "you" : "someone not named";
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
allMyDids = allAccounts.map((acc) => acc.did);
if (this.giverDid && !this.giverName) {
this.giverName = didInfo(
this.giverDid,
this.activeDid,
allMyDids,
allContacts,
);
}
if (this.recipientDid && !this.recipientName) {
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
);
}
} }
this.givenToProject = !!this.projectId; this.givenToProject = !!this.projectId;
this.givenToUser = this.givenToRecipient = !this.givenToProject && !!this.recipientDid;
!this.projectId && this.recipientDid === this.activeDid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
@@ -263,14 +313,12 @@ export default class GiftedDetails extends Vue {
if (this.projectId) { if (this.projectId) {
// console.log("Getting project name from cache", this.projectId); // console.log("Getting project name from cache", this.projectId);
const identity = await libsUtil.getIdentity(this.activeDid);
const project = await getPlanFromCache( const project = await getPlanFromCache(
this.projectId, this.projectId,
identity,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid,
); );
console.log("Got project name from cache", project);
this.projectName = project?.name this.projectName = project?.name
? "the project: " + project.name ? "the project: " + project.name
: "a project"; : "a project";
@@ -332,8 +380,7 @@ export default class GiftedDetails extends Vue {
return; return;
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
@@ -442,18 +489,6 @@ export default class GiftedDetails extends Vue {
await this.recordGive(); await this.recordGive();
} }
notifyUser(message: string) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: message,
},
3000,
);
}
notifyUserOfProject() { notifyUserOfProject() {
if (!this.projectId) { if (!this.projectId) {
this.$notify( this.$notify(
@@ -466,13 +501,38 @@ export default class GiftedDetails extends Vue {
3000, 3000,
); );
} else { } else {
// must be because givenToUser is true // must be because givenToRecipient is true
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "warning",
title: "Error", title: "Error",
text: "You cannot assign both to a project and to yourself.", text: "You cannot assign both to a project and to a recipient.",
},
3000,
);
}
}
notifyUserOfRecipient() {
if (!this.recipientDid) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "To assign to a recipient, you must open this dialog from a contact.",
},
3000,
);
} else {
// must be because givenToProject is true
this.$notify(
{
group: "alert",
type: "warning",
title: "Error",
text: "You cannot assign both to a recipient and to a project.",
}, },
3000, 3000,
); );
@@ -488,18 +548,14 @@ export default class GiftedDetails extends Vue {
*/ */
public async recordGive() { public async recordGive() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const recipientDid = this.givenToRecipient
const recipientDid = ? this.recipientDid
this.recipientDid === this.activeDid : undefined;
? this.givenToUser
? this.activeDid
: undefined
: this.recipientDid;
const projectId = this.givenToProject ? this.projectId : undefined; const projectId = this.givenToProject ? this.projectId : undefined;
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
this.giverDid, this.giverDid,
recipientDid, recipientDid,
this.description, this.description,
@@ -562,6 +618,24 @@ export default class GiftedDetails extends Vue {
} }
} }
constructGiveParam() {
const recipientDid = this.givenToRecipient ? this.recipientDid : undefined;
const projectId = this.givenToProject ? this.projectId : undefined;
const giveClaim = constructGive(
this.giverDid,
recipientDid,
this.description,
parseFloat(this.amountInput),
this.unitCode,
projectId,
this.offerId,
this.isTrade,
this.imageUrl,
);
const claimStr = JSON.stringify(giveClaim);
return claimStr;
}
// Helper functions for readability // Helper functions for readability
/** /**

View File

@@ -76,9 +76,6 @@
<p> <p>
Go Go
<router-link class="text-blue-500" to="/import-account">import your identifier</router-link>. <router-link class="text-blue-500" to="/import-account">import your identifier</router-link>.
If you don't want the old one, click "Advanced" and check the box to erase it.
(The erase option only shows if you have exactly one identifier.
For more in-depth surgery, you'll have to erase data from the browser or reinstall.)
</p> </p>
<h2 class="text-xl font-semibold">How do I add someone else?</h2> <h2 class="text-xl font-semibold">How do I add someone else?</h2>
@@ -86,8 +83,8 @@
<a href="/help-onboarding" target="_blank" class="text-blue-500"> <a href="/help-onboarding" target="_blank" class="text-blue-500">
Use these instructions. Use these instructions.
</a> </a>
To start scanning, go To start scanning, go to the
<router-link class="text-blue-500" to="/contact-qr">here.</router-link> <router-link class="text-blue-500" to="/contact-qr">contact-scanning page.</router-link>
</p> </p>
<p> <p>
If they are not nearby to scan QR codes, you each can tap on the QR code If they are not nearby to scan QR codes, you each can tap on the QR code
@@ -119,7 +116,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my non-secret, non-public text data? How do I backup my other private text data like settings & contacts?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@@ -133,7 +130,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my non-secret, non-public image? How do I backup my profile image?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@@ -143,7 +140,7 @@
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
How do I backup my public data? How do I backup other data I've posted?
</h2> </h2>
<ul class="list-disc list-outside ml-4"> <ul class="list-disc list-outside ml-4">
<li> <li>
@@ -180,6 +177,7 @@
<li> <li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page, Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for the Contacts & Settings Database "Import". click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
Beware that this will erase your existing contact & settings.
</li> </li>
</ul> </ul>
</div> </div>
@@ -340,7 +338,7 @@
<h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2> <h2 class="text-xl font-semibold">What are the terms & conditions and the privacy policy?</h2>
<p style="display:inline; align-items: center"> <p style="display:inline; align-items: center">
This work is public domain, governed by This work is public domain. If you like rules, reference
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer"> <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
<span class="text-blue-500 mr-1">CC0 1.0</span> <span class="text-blue-500 mr-1">CC0 1.0</span>
<img <img
@@ -366,6 +364,26 @@
</a> </a>
</p> </p>
<h2 class="text-xl font-semibold">How can I contribute?</h2>
<p>
If you have skills, contact us below.
If you have Bitcoin, donate to
<button
@click="
doCopyTwoSecRedo(
'bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma',
() => (showDidCopy = !showDidCopy)
)
"
class="text-blue-500 ml-2"
>
bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDidCopy">Copied</span>
For other donations, contact us.
</p>
<h2 class="text-xl font-semibold">Where can I read more?</h2> <h2 class="text-xl font-semibold">Where can I read more?</h2>
<p> <p>
This is part of the This is part of the
@@ -379,7 +397,7 @@
<p>{{ package.version }} ({{ commitHash }})</p> <p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
For any other questions, including removing all your data from the public ledger: For any other questions, like getting a new account or removing all your data from the public ledger:
</h2> </h2>
<p> <p>
Contact us at Contact us at
@@ -394,6 +412,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -405,5 +424,14 @@ export default class Help extends Vue {
package = Package; package = Package;
commitHash = import.meta.env.VITE_GIT_HASH; commitHash = import.meta.env.VITE_GIT_HASH;
showDidCopy = false;
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
} }
</script> </script>

View File

@@ -5,7 +5,7 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
Time Safari {{ AppString.APP_NAME }}
</h1> </h1>
<!-- prompt to install notifications --> <!-- prompt to install notifications -->
@@ -79,89 +79,113 @@
<!-- !isCreatingIdentifier --> <!-- !isCreatingIdentifier -->
<div <div
v-if="!activeDid" v-if="!activeDid"
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" class="bg-amber-200 rounded-md text-center px-4 py-3 mb-4"
> >
<p class="text-lg mb-3"> <div v-if="PASSKEYS_ENABLED">
Want to connect with your contacts, or share contributions or <p class="text-lg mb-3">
projects? Choose how to see info from your contacts or share contributions:
</p> </p>
<router-link <div class="flex justify-between">
:to="{ name: 'start' }" <button
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> @click="generateIdentifier()"
Create An Identifier >
</router-link> Let me start the easiest (with a passkey).
</button>
<router-link
:to="{ name: 'start' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Give me all the options.
</router-link>
</div>
</div>
<div v-else>
<p class="text-lg mb-3">
To recognize giving or collaborate, have someone register you:
</p>
<router-link
:to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
Share your contact info.
</router-link>
</div>
</div> </div>
<div <div v-else class="mb-4">
v-else-if="!isRegistered" <!-- activeDid -->
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
> <div
<!-- activeDid && !isRegistered --> v-if="!isRegistered"
Someone must register you before you can give kudos or make offers or class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
create projects... basically before doing anything.
<router-link
:to="{ name: 'contact-qr' }"
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
> >
Show Them Your Identifier Info <!-- activeDid && !isRegistered -->
</router-link> Someone must register you before you can give kudos or make offers
</div> or create projects... basically before doing anything.
<router-link
<div v-else> :to="{ name: 'contact-qr' }"
<!-- activeDid && isRegistered --> class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
>
<!-- show the actions for recognizing a give --> Show Them Your Identifier Info
<div class="mb-4"> </router-link>
<h2 class="text-xl font-bold">Record Something Given By:</h2>
</div> </div>
<ul <div v-else>
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5" <!-- activeDid && isRegistered -->
>
<li @click="openDialog()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-slate-300 rounded-md mb-1"
/>
<h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
Unnamed/Unknown
</h3>
</li>
<li
v-for="contact in allContacts.slice(0, 7)"
:key="contact.did"
@click="openDialog(contact)"
>
<EntityIcon
:contact="contact"
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ contact.name || contact.did }}
</h3>
</li>
</ul>
<div class="flex justify-between"> <!-- show the actions for recognizing a give -->
<router-link <div class="mb-4">
v-if="allContacts.length >= 7" <h2 class="text-xl font-bold">Record Something Given By:</h2>
:to="{ name: 'contact-gift' }" </div>
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5"
> >
Choose From All Contacts <li @click="openDialog()">
</router-link> <img
<button src="../assets/blank-square.svg"
@click="openGiftedPrompts()" class="mx-auto border border-slate-300 rounded-md mb-1"
class="block text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" />
> <h3
Ideas... class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
</button> >
Unnamed/Unknown
</h3>
</li>
<li
v-for="contact in allContacts.slice(0, 7)"
:key="contact.did"
@click="openDialog(contact)"
>
<EntityIcon
:contact="contact"
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ contact.name || contact.did }}
</h3>
</li>
</ul>
<div class="flex justify-between">
<router-link
v-if="allContacts.length >= 7"
:to="{ name: 'contact-gift' }"
class="block text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
>
Choose From All Contacts
</router-link>
<button
@click="openGiftedPrompts()"
class="block text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
>
Ideas...
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -305,10 +329,10 @@
<script lang="ts"> <script lang="ts">
import { UAParser } from "ua-parser-js"; import { UAParser } from "ua-parser-js";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import App from "../App.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import GiftedPrompts from "@/components/GiftedPrompts.vue"; import GiftedPrompts from "@/components/GiftedPrompts.vue";
@@ -316,9 +340,8 @@ import FeedFilters from "@/components/FeedFilters.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { AppString, NotificationIface, PASSKEYS_ENABLED } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { import {
BoundingBox, BoundingBox,
@@ -326,17 +349,17 @@ import {
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
Settings, Settings,
} from "@/db/tables/settings"; } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
contactForDid, contactForDid,
containsNonHiddenDid, containsNonHiddenDid,
didInfoForContact, didInfoForContact,
fetchEndorserRateLimits, fetchEndorserRateLimits,
getHeaders,
getPlanFromCache, getPlanFromCache,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util"; import { registerSaveAndActivatePasskey } from "@/libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord { interface GiveRecordWithContactInfo extends GiveSummaryRecord {
giver: { giver: {
@@ -354,6 +377,11 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
} }
@Component({ @Component({
computed: {
App() {
return App;
},
},
components: { components: {
GiftedDialog, GiftedDialog,
GiftedPrompts, GiftedPrompts,
@@ -367,6 +395,9 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
AppString = AppString;
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
@@ -374,6 +405,7 @@ export default class HomeView extends Vue {
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
givenName = "";
isAnyFeedFilterOn: boolean; isAnyFeedFilterOn: boolean;
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedFilteredByVisible = false; isFeedFilteredByVisible = false;
@@ -387,25 +419,6 @@ export default class HomeView extends Vue {
showShortcutBvc = false; showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
return identity; // may be null
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async mounted() { async mounted() {
try { try {
await accountsDB.open(); await accountsDB.open();
@@ -418,6 +431,7 @@ export default class HomeView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.givenName = settings?.firstName || "";
this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible; this.isFeedFilteredByVisible = !!settings?.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby; this.isFeedFilteredByNearby = !!settings?.filterFeedByNearby;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
@@ -426,21 +440,13 @@ export default class HomeView extends Vue {
this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = isAnyFeedFilterOn(settings);
if (this.allMyDids.length === 0) { // someone may have have registered after sharing contact info, so recheck
this.isCreatingIdentifier = true;
this.activeDid = await generateSaveAndActivateIdentity();
this.allMyDids = [this.activeDid];
this.isCreatingIdentifier = false;
}
// someone may have have registered after sharing contact info
if (!this.isRegistered && this.activeDid) { if (!this.isRegistered && this.activeDid) {
const identity = await this.getIdentity(this.activeDid);
try { try {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
this.apiServer, this.apiServer,
this.axios, this.axios,
identity as IIdentifier, this.activeDid,
); );
if (resp.status === 200) { if (resp.status === 200) {
// we just needed to know that they're registered // we just needed to know that they're registered
@@ -475,6 +481,15 @@ export default class HomeView extends Vue {
} }
} }
async generateIdentifier() {
this.isCreatingIdentifier = true;
const account = await registerSaveAndActivatePasskey(
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),
);
this.activeDid = account.did;
this.allMyDids = this.allMyDids.concat(this.activeDid);
this.isCreatingIdentifier = false;
}
resultsAreFiltered() { resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby; return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
} }
@@ -483,26 +498,6 @@ export default class HomeView extends Vue {
return "Notification" in window; return "Notification" in window;
} }
public async buildHeaders() {
const headers: HeadersInit = {
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (this.activeDid) {
if (identity) {
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
// only called when a setting was changed // only called when a setting was changed
async reloadFeedOnChange() { async reloadFeedOnChange() {
await db.open(); await db.open();
@@ -520,7 +515,7 @@ export default class HomeView extends Vue {
* Data loader used by infinite scroller * Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load * @param payload is the flag from the InfiniteScroll indicating if it should load
**/ **/
public async loadMoreGives(payload: boolean) { async loadMoreGives(payload: boolean) {
// Since feed now loads projects along the way, it takes longer // Since feed now loads projects along the way, it takes longer
// and the InfiniteScroll component triggers a load before finished. // and the InfiniteScroll component triggers a load before finished.
// One alternative is to totally separate the project link loading. // One alternative is to totally separate the project link loading.
@@ -542,7 +537,7 @@ export default class HomeView extends Vue {
} }
} }
public async updateAllFeed() { async updateAllFeed() {
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
await this.retrieveGives(this.apiServer, this.feedPreviousOldestId) await this.retrieveGives(this.apiServer, this.feedPreviousOldestId)
@@ -550,7 +545,6 @@ export default class HomeView extends Vue {
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; endOfResults = false;
// include the descriptions of the giver and receiver // include the descriptions of the giver and receiver
const identity = await this.getIdentity(this.activeDid);
for (const record: GiveSummaryRecord of results.data) { for (const record: GiveSummaryRecord of results.data) {
// similar code is in endorser-mobile utility.ts // similar code is in endorser-mobile utility.ts
// claim.claim happen for some claims wrapped in a Verifiable Credential // claim.claim happen for some claims wrapped in a Verifiable Credential
@@ -567,9 +561,9 @@ export default class HomeView extends Vue {
// We should display it immediately and then get the plan later. // We should display it immediately and then get the plan later.
const plan = await getPlanFromCache( const plan = await getPlanFromCache(
record.fulfillsPlanHandleId, record.fulfillsPlanHandleId,
identity,
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid,
); );
// check if the record should be filtered out // check if the record should be filtered out
@@ -650,7 +644,7 @@ export default class HomeView extends Vue {
* @param beforeId the earliest ID (of previous searches) to search earlier * @param beforeId the earliest ID (of previous searches) to search earlier
* @return claims in reverse chronological order * @return claims in reverse chronological order
*/ */
public async retrieveGives(endorserApiServer: string, beforeId?: string) { async retrieveGives(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch( const response = await fetch(
endorserApiServer + endorserApiServer +
@@ -658,7 +652,7 @@ export default class HomeView extends Vue {
beforeQuery, beforeQuery,
{ {
method: "GET", method: "GET",
headers: await this.buildHeaders(), headers: await getHeaders(this.activeDid),
}, },
); );

View File

@@ -39,24 +39,43 @@
<!-- Other Identity/ies --> <!-- Other Identity/ies -->
<ul class="mb-4"> <ul class="mb-4">
<li <li v-for="ident in otherIdentities" :key="ident.did">
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2" <div class="flex items-center justify-between mb-2">
v-for="ident in otherIdentities" <div
:key="ident.did" class="flex flex-grow items-center bg-slate-100 rounded-md px-4 py-3 mb-2 truncate cursor-pointer"
@click="switchAccount(ident.did)" @click="switchAccount(ident.did)"
> >
<fa <fa
v-if="ident.did === activeDid" v-if="ident.did === activeDid"
icon="circle-check" icon="circle-check"
class="fa-fw text-blue-600 text-xl mr-3" class="fa-fw text-blue-600 text-xl mr-3"
/> />
<fa v-else icon="circle" class="fa-fw text-slate-400 text-xl mr-3" /> <fa
<span class="overflow-hidden"> v-else
<h2 class="text-xl font-semibold mb-0"></h2> icon="circle"
<div class="text-sm text-slate-500 truncate"> class="fa-fw text-slate-400 text-xl mr-3"
<b>ID:</b> <code>{{ ident.did }}</code> />
<span class="flex-grow overflow-hidden">
<div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ ident.did }}</code>
</div>
</span>
</div> </div>
</span> <div>
<fa
v-if="ident.did === activeDid"
icon="trash-can"
class="text-slate-400 text-xl ml-2 mr-2 cursor-pointer"
@click="notifyCannotDelete()"
/>
<fa
v-else
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@click="deleteAccount(ident.id)"
/>
</div>
</div>
</li> </li>
</ul> </ul>
@@ -81,9 +100,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString, NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@@ -91,14 +109,11 @@ import QuickNav from "@/components/QuickNav.vue";
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
Constants = AppString;
public accounts: typeof AccountsSchema;
public activeDid = ""; public activeDid = "";
public activeDidInIdentities = false; public activeDidInIdentities = false;
public apiServer = ""; public apiServer = "";
public apiServerInput = ""; public apiServerInput = "";
public otherIdentities: Array<{ did: string }> = []; public otherIdentities: Array<{ id: string; did: string }> = [];
public showContactGives = false;
async created() { async created() {
try { try {
@@ -107,20 +122,14 @@ export default class IdentitySwitcherView extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = settings?.apiServer || "";
this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
for (let n = 0; n < accounts.length; n++) { for (let n = 0; n < accounts.length; n++) {
try { const acct = accounts[n];
const did = accounts[n]["did"]; this.otherIdentities.push({ id: acct.id as string, did: acct.did });
this.otherIdentities.push({ did: did }); if (acct.did && this.activeDid === acct.did) {
if (did && this.activeDid === did) { this.activeDidInIdentities = true;
this.activeDidInIdentities = true;
}
} catch (err) {
console.error("Error parsing identity:", err);
continue;
} }
} }
} catch (err) { } catch (err) {
@@ -148,5 +157,36 @@ export default class IdentitySwitcherView extends Vue {
}); });
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
} }
async deleteAccount(id: string) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete Identity?",
text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)",
onYes: async () => {
await accountsDB.open();
await accountsDB.accounts.delete(id);
this.otherIdentities = this.otherIdentities.filter(
(ident) => ident.id !== id,
);
},
},
-1,
);
}
notifyCannotDelete() {
this.$notify(
{
group: "alert",
type: "warning",
title: "Cannot Delete",
text: "You cannot delete the active identity.",
},
3000,
);
}
} }
</script> </script>

View File

@@ -17,7 +17,7 @@
<div> <div>
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Will increment the maximum derivation path from the existing seed. Will increment the maximum known derivation path from the existing seed.
</p> </p>
<p v-if="didArrays.length > 1"> <p v-if="didArrays.length > 1">
@@ -75,7 +75,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
nextDerivationPath, nextDerivationPath,
} from "../libs/crypto"; } from "@/libs/crypto";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";

View File

@@ -68,8 +68,6 @@ export default class NewEditAccountView extends Vue {
firstName: this.givenName, firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3 lastName: "", // deprecated, pre v 0.1.3
}); });
localStorage.setItem("firstName", this.givenName as string);
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.back(); this.$router.back();
} }

View File

@@ -174,21 +174,21 @@
<script lang="ts"> <script lang="ts">
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import {
createEndorserJwtVcFromClaim,
PlanVerifiableCredential,
} from "@/libs/endorserServer";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
@Component({ @Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav }, components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
@@ -227,33 +227,6 @@ export default class NewEditProjectView extends Vue {
zoneName = DateTime.local().zoneName; zoneName = DateTime.local().zoneName;
zoom = 2; zoom = 2;
libsUtil = libsUtil;
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async mounted() { async mounted() {
await accountsDB.open(); await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
@@ -267,23 +240,17 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) { if (this.numAccounts === 0) {
this.errNote("There was a problem loading your account info."); this.errNote("There was a problem loading your account info.");
} else { } else {
const identity = await this.getIdentity(this.activeDid); this.loadProject(this.activeDid);
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service. Switch your ID.",
);
}
this.loadProject(identity);
} }
} }
} }
async loadProject(identity: IIdentifier) { async loadProject(userDid: string) {
const url = const url =
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const token = await accessToken(identity); const token = await accessToken(userDid);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
@@ -342,8 +309,7 @@ export default class NewEditProjectView extends Vue {
return; return;
} }
try { try {
const identity = await libsUtil.getIdentity(this.activeDid); const token = await accessToken(this.activeDid);
const token = await accessToken(identity);
const response = await this.axios.delete( const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + DEFAULT_IMAGE_API_SERVER +
"/image/" + "/image/" +
@@ -395,7 +361,7 @@ export default class NewEditProjectView extends Vue {
} }
} }
private async saveProject(identity: IIdentifier) { private async saveProject(issuerDid: string) {
// Make a claim // Make a claim
const vcClaim: PlanVerifiableCredential = this.fullClaim; const vcClaim: PlanVerifiableCredential = this.fullClaim;
if (this.projectId) { if (this.projectId) {
@@ -446,110 +412,88 @@ export default class NewEditProjectView extends Vue {
} else { } else {
delete vcClaim.startTime; delete vcClaim.startTime;
} }
// Make a payload for the claim const vcJwt = await createEndorserJwtVcFromClaim(issuerDid, vcClaim);
const vcPayload = {
vc: { // Make the xhr request payload
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"], const payload = JSON.stringify({ jwtEncoded: vcJwt });
credentialSubject: vcClaim, const url = this.apiServer + "/api/v2/claim";
}, const token = await accessToken(issuerDid);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
}; };
// create a signature using private key of identity
if (identity.keys[0].privateKeyHex != null) {
const privateKeyHex: string = identity.keys[0].privateKeyHex;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success?.handleId) {
this.errorMessage = "";
const payload = JSON.stringify({ jwtEncoded: vcJwt }); useAppStore()
const url = this.apiServer + "/api/v2/claim"; .setProjectId(resp.data.success.handleId)
const token = await accessToken(identity); .then(() => {
const headers = { this.$router.push({ name: "project" });
"Content-Type": "application/json", });
Authorization: "Bearer " + token, } else {
}; console.error(
"Got unexpected 'data' inside response from server",
try { resp,
const resp = await this.axios.post(url, payload, { headers }); );
if (resp.data?.success?.handleId) { this.$notify(
this.errorMessage = ""; {
group: "alert",
useAppStore() type: "danger",
.setProjectId(resp.data.success.handleId) title: "Error Saving Idea",
.then(() => { text: "Server did not save the idea. Try again.",
this.$router.push({ name: "project" }); },
}); -1,
} else { );
console.error(
"Got unexpected 'data' inside response from server",
resp,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Saving Idea",
text: "Server did not save the idea. Try again.",
},
-1,
);
}
} catch (error) {
let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError<{
error?: { message?: string };
}>;
if (serverError) {
console.error("Got error from server", serverError);
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
userMessage =
(serverError.response?.data?.error?.message as string) ||
userMessage;
this.$notify(
{
group: "alert",
type: "danger",
title: "User Message",
text: userMessage,
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Message",
text: JSON.stringify(serverError.toJSON()),
},
-1,
);
}
} else {
console.error(
"Here's the full error trying to save the claim:",
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Claim Error",
text: error as string,
},
-1,
);
}
// Now set that error for the user to see.
this.errorMessage = userMessage;
} }
} catch (error) {
let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError<{
error?: { message?: string };
}>;
if (serverError) {
console.error("Got error from server", serverError);
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
userMessage =
(serverError.response?.data?.error?.message as string) ||
userMessage;
this.$notify(
{
group: "alert",
type: "danger",
title: "User Message",
text: userMessage,
},
-1,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Server Message",
text: JSON.stringify(serverError.toJSON()),
},
-1,
);
}
} else {
console.error("Here's the full error trying to save the claim:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Claim Error",
text: error as string,
},
-1,
);
}
// Now set that error for the user to see.
this.errorMessage = userMessage;
} }
} }
@@ -560,8 +504,7 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) { if (this.numAccounts === 0) {
console.error("Error: there is no account."); console.error("Error: there is no account.");
} else { } else {
const identity = await this.getIdentity(this.activeDid); this.saveProject(this.activeDid);
this.saveProject(identity);
} }
} }

View File

@@ -256,6 +256,7 @@
contact above.) contact above.)
</div> </div>
<!-- similar to gift display below -->
<ul v-else class="text-sm border-t border-slate-300"> <ul v-else class="text-sm border-t border-slate-300">
<li <li
v-for="give in givesToThis" v-for="give in givesToThis"
@@ -263,8 +264,8 @@
class="py-1.5 border-b border-slate-300" class="py-1.5 border-b border-slate-300"
> >
<div class="flex justify-between gap-4"> <div class="flex justify-between gap-4">
<span <span>
><fa icon="user" class="fa-fw text-slate-400"></fa> <fa icon="user" class="fa-fw text-slate-400" />
{{ {{
serverUtil.didInfo( serverUtil.didInfo(
give.agentDid, give.agentDid,
@@ -308,12 +309,62 @@
</div> </div>
<div class="grid items-start grid-cols-1 gap-4"> <div class="grid items-start grid-cols-1 gap-4">
<div
v-if="givesProvidedByThis.length > 0"
class="bg-slate-100 px-4 py-3 rounded-md"
>
<h3 class="text-sm uppercase font-semibold mb-3 border-b">
Individuals Getting Contributions From This
</h3>
<!-- similar to gift display above -->
<ul class="text-sm border-t border-slate-300">
<li
v-for="give in givesProvidedByThis"
:key="give.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span>
{{
serverUtil.didInfo(
give.agentDid,
activeDid,
allMyDids,
allContacts,
)
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<fa
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a>
</li>
</ul>
<div v-if="givesProvidedByHitLimit" class="text-center">
<button @click="loadGivesProvidedBy()">Load More</button>
</div>
</div>
<div <div
v-if="fulfillersToThis.length > 0" v-if="fulfillersToThis.length > 0"
class="bg-slate-100 px-4 py-3 rounded-md" class="bg-slate-100 px-4 py-3 rounded-md"
> >
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Contributions To This Idea Projects That Contribute To This
</h3> </h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" --> <!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center"> <div class="text-center">
@@ -325,13 +376,15 @@
{{ plan.name }} {{ plan.name }}
</button> </button>
</div> </div>
<div v-if="fulfillersToHitLimit" class="text-center">Load More</div> <div v-if="fulfillersToHitLimit" class="text-center">
<button @click="loadPlanFulfillersTo()">Load More</button>
</div>
</div> </div>
</div> </div>
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md"> <div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3"> <h3 class="text-sm uppercase font-semibold mb-3">
Contributions From This Idea Projects Getting Contributions From This
</h3> </h3>
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" --> <!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center"> <div class="text-center">
@@ -349,8 +402,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
@@ -369,6 +421,7 @@ import * as libsUtil from "@/libs/util";
import { import {
BLANK_GENERIC_SERVER_RECORD, BLANK_GENERIC_SERVER_RECORD,
GenericCredWrapper, GenericCredWrapper,
getHeaders,
GiverReceiverInputInfo, GiverReceiverInputInfo,
GiveSummaryRecord, GiveSummaryRecord,
OfferSummaryRecord, OfferSummaryRecord,
@@ -401,6 +454,8 @@ export default class ProjectViewView extends Vue {
fulfillersToHitLimit = false; fulfillersToHitLimit = false;
givesToThis: Array<GiveSummaryRecord> = []; givesToThis: Array<GiveSummaryRecord> = [];
givesHitLimit = false; givesHitLimit = false;
givesProvidedByThis: Array<GiveSummaryRecord> = [];
givesProvidedByHitLimit = false;
imageUrl = ""; imageUrl = "";
issuer = ""; issuer = "";
latitude = 0; latitude = 0;
@@ -429,24 +484,12 @@ export default class ProjectViewView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr: Account[] = await accounts?.toArray(); const accountsArr: Account[] = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); this.allMyDids = accountsArr.map((acc) => acc.did);
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse((account?.identity as string) || "null");
const pathParam = window.location.pathname.substring("/project/".length); const pathParam = window.location.pathname.substring("/project/".length);
if (pathParam) { if (pathParam) {
this.projectId = decodeURIComponent(pathParam); this.projectId = decodeURIComponent(pathParam);
} }
this.loadProject(this.projectId, identity); this.loadProject(this.projectId, this.activeDid);
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
return identity;
} }
onEditClick() { onEditClick() {
@@ -466,18 +509,12 @@ export default class ProjectViewView extends Vue {
this.expanded = false; this.expanded = false;
} }
async loadProject(projectId: string, identity: IIdentifier) { async loadProject(projectId: string, userDid: string) {
this.projectId = projectId; this.projectId = projectId;
const url = const url =
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId); this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(userDid);
"Content-Type": "application/json",
};
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -540,13 +577,15 @@ export default class ProjectViewView extends Vue {
this.loadGives(); this.loadGives();
this.loadGivesProvidedBy();
this.loadOffers(); this.loadOffers();
this.loadFulfillersTo(); this.loadPlanFulfillersTo();
// now load fulfilled-by, a single project // now load fulfilled-by, a single project
if (identity) { if (this.activeDid) {
const token = await accessToken(identity); const token = await accessToken(this.activeDid);
headers["Authorization"] = "Bearer " + token; headers["Authorization"] = "Bearer " + token;
} }
const fulfilledByUrl = const fulfilledByUrl =
@@ -598,15 +637,7 @@ export default class ProjectViewView extends Vue {
} }
const givesInUrl = givesUrl + postfix; const givesInUrl = givesUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(givesInUrl, { headers }); const resp = await this.axios.get(givesInUrl, { headers });
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@@ -653,15 +684,7 @@ export default class ProjectViewView extends Vue {
} }
const offersInUrl = offersUrl + postfix; const offersInUrl = offersUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(offersInUrl, { headers }); const resp = await this.axios.get(offersInUrl, { headers });
if (resp.status === 200 && resp.data.data) { if (resp.status === 200 && resp.data.data) {
@@ -696,7 +719,7 @@ export default class ProjectViewView extends Vue {
} }
} }
async loadFulfillersTo() { async loadPlanFulfillersTo() {
const fulfillsUrl = const fulfillsUrl =
this.apiServer + this.apiServer +
"/api/v2/report/planFulfillersToPlan?planHandleId=" + "/api/v2/report/planFulfillersToPlan?planHandleId=" +
@@ -709,15 +732,7 @@ export default class ProjectViewView extends Vue {
} }
const fulfillsInUrl = fulfillsUrl + postfix; const fulfillsInUrl = fulfillsUrl + postfix;
const headers: RawAxiosRequestHeaders = { const headers = await getHeaders(this.activeDid);
"Content-Type": "application/json",
};
const identity = await this.getIdentity(this.activeDid);
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(fulfillsInUrl, { headers }); const resp = await this.axios.get(fulfillsInUrl, { headers });
if (resp.status === 200) { if (resp.status === 200) {
@@ -752,6 +767,56 @@ export default class ProjectViewView extends Vue {
} }
} }
async loadGivesProvidedBy() {
const providedByUrl =
this.apiServer +
"/api/v2/report/givesProvidedBy?providerId=" +
encodeURIComponent(this.projectId);
let postfix = "";
if (this.givesProvidedByThis.length > 0) {
postfix =
"&beforeId=" +
this.givesProvidedByThis[this.givesProvidedByThis.length - 1].jwtId;
}
const providedByFullUrl = providedByUrl + postfix;
const headers = await getHeaders(this.activeDid);
try {
const resp = await this.axios.get(providedByFullUrl, { headers });
if (resp.status === 200) {
this.givesProvidedByThis = this.givesProvidedByThis.concat(
resp.data.data,
);
this.givesProvidedByHitLimit = resp.data.hitLimit;
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to retrieve gives that were provided by this project.",
},
5000,
);
}
} catch (error: unknown) {
const serverError = error as AxiosError;
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong retrieving gives that were provided by this project.",
},
5000,
);
console.error(
"Something went wrong retrieving gives that were provided by this project:",
serverError.message,
);
}
}
/** /**
* Handle clicking on a project entry found in the list * Handle clicking on a project entry found in the list
* @param id of the project * @param id of the project
@@ -762,7 +827,7 @@ export default class ProjectViewView extends Vue {
path: "/project/" + encodeURIComponent(projectId), path: "/project/" + encodeURIComponent(projectId),
}; };
this.$router.push(route); this.$router.push(route);
this.loadProject(projectId, await this.getIdentity(this.activeDid)); this.loadProject(projectId, this.activeDid);
} }
getOpenStreetMapUrl() { getOpenStreetMapUrl() {
@@ -906,7 +971,7 @@ export default class ProjectViewView extends Vue {
}; };
const result = await serverUtil.createAndSubmitClaim( const result = await serverUtil.createAndSubmitClaim(
confirmationClaim, confirmationClaim,
await this.getIdentity(this.activeDid), this.activeDid,
this.apiServer, this.apiServer,
this.axios, this.axios,
); );

View File

@@ -235,7 +235,6 @@ import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import ProjectIcon from "@/components/ProjectIcon.vue"; import ProjectIcon from "@/components/ProjectIcon.vue";
@@ -255,9 +254,9 @@ export default class ProjectsView extends Vue {
); );
} }
activeDid = "";
apiServer = ""; apiServer = "";
projects: PlanData[] = []; projects: PlanData[] = [];
currentIid: IIdentifier;
isLoading = false; isLoading = false;
isRegistered = false; isRegistered = false;
numAccounts = 0; numAccounts = 0;
@@ -271,7 +270,7 @@ export default class ProjectsView extends Vue {
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid: string = (settings?.activeDid as string) || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
@@ -281,7 +280,6 @@ export default class ProjectsView extends Vue {
console.error("No accounts found."); console.error("No accounts found.");
this.errNote("You need an identifier to load your projects."); this.errNote("You need an identifier to load your projects.");
} else { } else {
this.currentIid = await this.getIdentity(activeDid);
await this.loadOffers(); await this.loadOffers();
} }
} catch (err) { } catch (err) {
@@ -342,7 +340,7 @@ export default class ProjectsView extends Vue {
if (this.projects.length > 0 && payload) { if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1]; const latestProject = this.projects[this.projects.length - 1];
await this.loadProjects( await this.loadProjects(
this.currentIid, this.activeDid,
`beforeId=${latestProject.rowid}`, `beforeId=${latestProject.rowid}`,
); );
} }
@@ -350,32 +348,15 @@ export default class ProjectsView extends Vue {
/** /**
* Load projects initially * Load projects initially
* @param identifier of the user * @param issuerDid of the user
* @param urlExtra additional url parameters in a string * @param urlExtra additional url parameters in a string
**/ **/
async loadProjects(identifier?: IIdentifier, urlExtra: string = "") { async loadProjects(activeDid?: string, urlExtra: string = "") {
const identity = identifier || this.currentIid;
const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`; const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
const token: string = await accessToken(identity); const token: string = await accessToken(activeDid);
await this.projectDataLoader(url, token); await this.projectDataLoader(url, token);
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identifier available.",
);
}
return identity;
}
/** /**
* Handle clicking on a project entry found in the list * Handle clicking on a project entry found in the list
* @param id of the project * @param id of the project
@@ -462,19 +443,18 @@ export default class ProjectsView extends Vue {
async loadMoreOfferData(payload: boolean) { async loadMoreOfferData(payload: boolean) {
if (this.offers.length > 0 && payload) { if (this.offers.length > 0 && payload) {
const latestOffer = this.offers[this.offers.length - 1]; const latestOffer = this.offers[this.offers.length - 1];
await this.loadOffers(this.currentIid, `&beforeId=${latestOffer.jwtId}`); await this.loadOffers(this.activeDid, `&beforeId=${latestOffer.jwtId}`);
} }
} }
/** /**
* Load offers initially * Load offers initially
* @param identifier of the user * @param issuerDid of the user
* @param urlExtra additional url parameters in a string * @param urlExtra additional url parameters in a string
**/ **/
async loadOffers(identifier?: IIdentifier, urlExtra: string = "") { async loadOffers(issuerDid?: string, urlExtra: string = "") {
const identity = identifier || this.currentIid; const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${issuerDid}${urlExtra}`;
const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${identity.did}${urlExtra}`; const token: string = await accessToken(issuerDid);
const token: string = await accessToken(identity);
await this.offerDataLoader(url, token); await this.offerDataLoader(url, token);
} }

View File

@@ -124,7 +124,6 @@ export default class QuickActionBvcBeginView extends Vue {
try { try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr); const hoursNum = libsUtil.numberOrZero(this.hoursStr);
const identity = await libsUtil.getIdentity(activeDid);
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
@@ -134,7 +133,7 @@ export default class QuickActionBvcBeginView extends Vue {
const timeResult = await createAndSubmitGive( const timeResult = await createAndSubmitGive(
axios, axios,
apiServer, apiServer,
identity, activeDid,
activeDid, activeDid,
undefined, undefined,
undefined, undefined,
@@ -165,7 +164,7 @@ export default class QuickActionBvcBeginView extends Vue {
if (this.attended) { if (this.attended) {
const attendResult = await createAndSubmitClaim( const attendResult = await createAndSubmitClaim(
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate), bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate),
identity, activeDid,
apiServer, apiServer,
axios, axios,
); );

View File

@@ -138,28 +138,25 @@
import axios from "axios"; import axios from "axios";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { import {
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription, claimSpecialDescription,
containsHiddenDid, containsHiddenDid,
createAndSubmitConfirmation, createAndSubmitConfirmation,
createAndSubmitGive, createAndSubmitGive,
ErrorResult,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
getHeaders,
ErrorResult,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({ @Component({
methods: { claimSpecialDescription }, methods: { claimSpecialDescription },
@@ -213,16 +210,7 @@ export default class QuickActionBvcBeginView extends Vue {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
const account: Account | undefined = await accountsDB.accounts const headers = await getHeaders(this.activeDid);
.where("did")
.equals(this.activeDid)
.first();
const identity: IIdentifier = JSON.parse(
(account?.identity as string) || "null",
);
const headers = {
Authorization: "Bearer " + (await accessToken(identity)),
};
try { try {
const response = await fetch( const response = await fetch(
this.apiServer + this.apiServer +
@@ -275,8 +263,6 @@ export default class QuickActionBvcBeginView extends Vue {
async record() { async record() {
try { try {
const identity = await libsUtil.getIdentity(this.activeDid);
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
// in parallel, make a confirmation for each selected claim and send them all to the server // in parallel, make a confirmation for each selected claim and send them all to the server
@@ -288,9 +274,8 @@ export default class QuickActionBvcBeginView extends Vue {
if (!record) { if (!record) {
return { type: "error", error: "Record not found." }; return { type: "error", error: "Record not found." };
} }
const identity = await libsUtil.getIdentity(this.activeDid);
return createAndSubmitConfirmation( return createAndSubmitConfirmation(
identity, this.activeDid,
record.claim as GenericVerifiableCredential, record.claim as GenericVerifiableCredential,
record.id, record.id,
record.handleId, record.handleId,
@@ -324,7 +309,7 @@ export default class QuickActionBvcBeginView extends Vue {
const giveResult = await createAndSubmitGive( const giveResult = await createAndSubmitGive(
axios, axios,
this.apiServer, this.apiServer,
identity, this.activeDid,
undefined, undefined,
this.activeDid, this.activeDid,
this.description, this.description,

View File

@@ -65,7 +65,6 @@ import {
} from "@/constants/app"; } from "@/constants/app";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { getIdentity } from "@/libs/util";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
@Component({ components: { PhotoDialog, QuickNav } }) @Component({ components: { PhotoDialog, QuickNav } })
@@ -152,8 +151,7 @@ export default class SharedPhotoView extends Vue {
let result; let result;
try { try {
// send the image to the server // send the image to the server
const identifier = await getIdentity(this.activeDid as string); const token = await accessToken(this.activeDid);
const token = await accessToken(identifier);
const headers = { const headers = {
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };

View File

@@ -17,7 +17,7 @@
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Start Here Generate an Identity
</h1> </h1>
</div> </div>
@@ -25,33 +25,57 @@
<div id="start-question" class="mt-8"> <div id="start-question" class="mt-8">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<p class="text-center text-xl font-light"> <p class="text-center text-xl font-light">
Do you want a new identifier of your own? How do you want to create this identifier?
</p> </p>
<p class="text-center font-light"> <p class="text-center font-light mt-6">
If you haven't used this before, click "Yes" to generate a new A <strong>passkey</strong> is easy to manage, though it is less
identifier. interoperable with other systems for advanced uses.
<a
href="https://www.perplexity.ai/search/what-are-passkeys-v2SHV3yLQlyA2CYH6.Nvhg"
target="_blank"
>
<fa icon="info-circle" class="fa-fw text-blue-500" />
</a>
</p> </p>
<p class="text-center mb-4 font-light"> <p class="text-center font-light mt-4">
Only click "No" if you have a seed of 12 or 24 words generated A <strong>new seed</strong> allows you full control over the keys,
elsewhere. though you are responsible for backups.
<a
href="https://www.perplexity.ai/search/what-is-a-seed-phrase-OqiP9foVRXidr_2le5OFKA"
target="_blank"
>
<fa icon="info-circle" class="fa-fw text-blue-500" />
</a>
</p> </p>
<a <div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
@click="onClickYes()" <a
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2" @click="onClickNewPasskey()"
> class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
Yes, generate one >
</a> Generate one with a passkey
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> </a>
<a
@click="onClickNewSeed()"
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
>
Generate one with a new seed
</a>
</div>
<p class="text-center font-light mt-4">
You can also import an existing seed or derive a new address from an
existing seed.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
<a <a
@click="onClickNo()" @click="onClickNo()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md cursor-pointer"
> >
No, I have a seed You have a seed
</a> </a>
<a <a
v-if="numAccounts > 0" v-if="numAccounts > 0"
@click="onClickDerive()" @click="onClickDerive()"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md cursor-pointer"
> >
Derive new address from existing seed Derive new address from existing seed
</a> </a>
@@ -64,23 +88,38 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB } from "@/db/index"; import { AppString } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { registerSaveAndActivatePasskey } from "@/libs/util";
@Component({ @Component({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {
givenName = "";
numAccounts = 0; numAccounts = 0;
async mounted() { async mounted() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.givenName = settings?.firstName || "";
await accountsDB.open(); await accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public onClickYes() { public onClickNewSeed() {
this.$router.push({ name: "new-identifier" }); this.$router.push({ name: "new-identifier" });
} }
public async onClickNewPasskey() {
const keyName =
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
await registerSaveAndActivatePasskey(keyName);
this.$router.push({ name: "account" });
}
public onClickNo() { public onClickNo() {
this.$router.push({ name: "import-account" }); this.$router.push({ name: "import-account" });
} }

View File

@@ -173,59 +173,65 @@
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2> <h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results. See console for results.
<br/> <br />
Active DID: {{ activeDid }} See existing passkeys in Chrome at: chrome://settings/passkeys
{{ credIdHex ? "has passkey ID" : "has no passkey ID" }} <br />
Active DID: {{ activeDid || "nothing, which" }}
{{ credIdHex ? "has a passkey ID" : "has no passkey ID" }}
<div> <div>
Register Register Passkey
<button <button
@click="register()" @click="register()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
</div> </div>
<div> <div>
Create Create JWT
<button <button
@click="createJwtSimplewebauthn()" @click="createJwtSimplewebauthn()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
<button <button
@click="createJwtNavigator()" @click="createJwtNavigator()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Navigator Navigator
</button> </button>
</div> </div>
<div v-if="jwt"> <div v-if="jwt">
Verify Verify New JWT
<button <button
@click="verifySimplewebauthn()" @click="verifySimplewebauthn()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Simplewebauthn Simplewebauthn
</button> </button>
<button <button
@click="verifyWebCrypto()" @click="verifyWebCrypto()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
WebCrypto WebCrypto
</button> </button>
<button <button
@click="verifyP256()" @click="verifyP256()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
p256 - broken p256 - broken
</button> </button>
</div> </div>
<div v-else>Verify New JWT -- requires creation first</div>
<button <button
@click="verifyMyJwt()" @click="verifyMyJwt()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" class="font-bold uppercase bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
> >
Verify Mine Verify Hard-Coded JWT
</button> </button>
</div> </div>
</section> </section>
@@ -238,16 +244,17 @@ import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { AppString, NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as vcLib from "@/libs/crypto/vc";
import { import {
createPeerDid,
PeerSetup, PeerSetup,
registerCredential,
verifyJwtP256, verifyJwtP256,
verifyJwtSimplewebauthn, verifyJwtSimplewebauthn,
verifyJwtWebCrypto, verifyJwtWebCrypto,
} from "@/libs/didPeer"; } from "@/libs/crypto/vc/passkeyDidPeer";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import {AccountKeyInfo, getAccount, registerAndSavePasskey} from "@/libs/util";
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@@ -263,6 +270,8 @@ const TEST_PAYLOAD = {
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
// for file import // for file import
fileName?: string; fileName?: string;
@@ -294,7 +303,7 @@ export default class Help extends Vue {
} }
async uploadFile(event: Event) { async uploadFile(event: Event) {
inputFileNameRef.value = event.target.files[0]; inputFileNameRef.value = event.target?.["files"][0];
// https://developer.mozilla.org/en-US/docs/Web/API/File // https://developer.mozilla.org/en-US/docs/Web/API/File
// ... plus it has a `type` property from my testing // ... plus it has a `type` property from my testing
const file = inputFileNameRef.value; const file = inputFileNameRef.value;
@@ -324,21 +333,41 @@ export default class Help extends Vue {
} }
public async register() { public async register() {
const cred = await registerCredential(this.userName); const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
const publicKeyBytes = cred.publicKeyBytes; if (!this.userName) {
this.activeDid = createPeerDid(publicKeyBytes as Uint8Array); this.$notify(
this.credIdHex = cred.credIdHex as string; {
group: "modal",
await accountsDB.open(); type: "confirm",
await accountsDB.accounts.add({ title: "No Name",
dateCreated: new Date().toISOString(), text: "You should have a name to attach to this passkey. Would you like to enter your own name first?",
did: this.activeDid, onNo: async () => {
passkeyCredIdHex: this.credIdHex, this.userName = DEFAULT_USERNAME;
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"), },
});`` onYes: async () => {
this.$router.push({ name: "new-edit-account" });
},
noText: "try again and use " + DEFAULT_USERNAME,
},
-1,
);
return;
}
const account = await registerAndSavePasskey(
AppString.APP_NAME + " - " + this.userName,
);
this.activeDid = account.did;
this.credIdHex = account.passkeyCredIdHex;
} }
public async createJwtSimplewebauthn() { public async createJwtSimplewebauthn() {
const account: AccountKeyInfo | undefined = await getAccount(
this.activeDid || "",
);
if (!vcLib.isFromPasskey(account)) {
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
return;
}
this.peerSetup = new PeerSetup(); this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtSimplewebauthn( this.jwt = await this.peerSetup.createJwtSimplewebauthn(
this.activeDid as string, this.activeDid as string,
@@ -349,6 +378,13 @@ export default class Help extends Vue {
} }
public async createJwtNavigator() { public async createJwtNavigator() {
const account: AccountKeyInfo | undefined = await getAccount(
this.activeDid || "",
);
if (!vcLib.isFromPasskey(account)) {
alert(`The DID ${this.activeDid} is not passkey-enabled.`);
return;
}
this.peerSetup = new PeerSetup(); this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtNavigator( this.jwt = await this.peerSetup.createJwtNavigator(
this.activeDid as string, this.activeDid as string,
@@ -360,44 +396,46 @@ export default class Help extends Vue {
public async verifyP256() { public async verifyP256() {
const decoded = await verifyJwtP256( const decoded = await verifyJwtP256(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid as string, this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup?.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array, this.peerSetup?.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString, this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
this.peerSetup.signature as Base64URLString, this.peerSetup?.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifySimplewebauthn() { public async verifySimplewebauthn() {
const decoded = await verifyJwtSimplewebauthn( const decoded = await verifyJwtSimplewebauthn(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid as string, this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup?.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array, this.peerSetup?.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString, this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
this.peerSetup.signature as Base64URLString, this.peerSetup?.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifyWebCrypto() { public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid as string, this.activeDid as string,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup?.authenticatorData as ArrayBuffer,
this.peerSetup.challenge as Uint8Array, this.peerSetup?.challenge as Uint8Array,
this.peerSetup.clientDataJsonBase64Url as Base64URLString, this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
this.peerSetup.signature as Base64URLString, this.peerSetup?.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
} }
public async verifyMyJwt() { public async verifyMyJwt() {
const did =
"did:peer:0zKMFjvUgYrM1hXwDciYHiA9MxXtJPXnRLJvqoMNAKoDLX9pKMWLb3VDsgua1p2zW1xXRsjZSTNsfvMnNyMS7dB4k7NAhFwL3pXBrBXgyYJ9ri";
const jwt = const jwt =
"eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE"; "eyJ0eXAiOiJKV0FOVCIsImFsZyI6IkVTMjU2In0.eyJBdXRoZW50aWNhdGlvbkRhdGFCNjRVUkwiOiJTWllONVlnT2pHaDBOQmNQWkhaZ1c0X2tycm1paGpMSG1Wenp1b01kbDJNRkFBQUFBQSIsIkNsaWVudERhdGFKU09OQjY0VVJMIjoiZXlKMGVYQmxJam9pZDJWaVlYVjBhRzR1WjJWMElpd2lZMmhoYkd4bGJtZGxJam9pWlhsS01sbDVTVFpsZVVwcVkyMVdhMXBYTlRCaFYwWnpWVE5XYVdGdFZtcGtRMGsyWlhsS1FWa3lPWFZrUjFZMFpFTkpOa2x0YURCa1NFSjZUMms0ZG1NeVRtOWFWekZvVEcwNWVWcDVTWE5KYTBJd1pWaENiRWxxYjJsU01td3lXbFZHYW1SSGJIWmlhVWx6U1cxU2JHTXlUbmxoV0VJd1lWYzVkVWxxYjJsalIydzJaVzFGYVdaWU1ITkpiV3hvWkVOSk5rMVVZM2hQUkZVMFRtcHJOVTFEZDJsaFdFNTZTV3B2YVZwSGJHdFBia0pzV2xoSk5rMUljRXhVVlZweFpHeFdibGRZU2s1TlYyaFpaREJTYW1GV2JFbGhWVVUxVkZob1dXUkZjRkZYUnpWVFZFVndNbU5YT1U1VWEwWk1ZakJTVFZkRWJIZFRNREZZVkVkSmVsWnJVbnBhTTFab1RWaEJlV1ZzWTNobFJtaFRZekp3WVZVeFVrOWpNbG95VkZjMVQyVlZNVlJPTWxKRFRrZHpNMVJyUm05U2JtUk5UVE5DV1ZGdVNrTlhSMlExVjFWdk5XTnRhMmxtVVNJc0ltOXlhV2RwYmlJNkltaDBkSEE2THk5c2IyTmhiR2h2YzNRNk9EQTRNQ0lzSW1OeWIzTnpUM0pwWjJsdUlqcG1ZV3h6WlgwIiwiaWF0IjoxNzE4NTg2OTkyLCJpc3MiOiJkaWQ6cGVlcjowektNRmp2VWdZck0xaFh3RGNpWUhpQTlNeFh0SlBYblJMSnZxb01OQUtvRExYOXBLTVdMYjNWRHNndWExcDJ6VzF4WFJzalpTVE5zZnZNbk55TVM3ZEI0azdOQWhGd0wzcFhCckJYZ3lZSjlyaSJ9.MEUCIQDJyCTbMPIFnuBoW3FYnlgtDEIHZ2OrkCEvqVnHU7kJDQIgVxjBjfW1TwQfcSOYwK8Z7AdCWGJlyxtLEsrnPif7caE";
const pieces = jwt.split("."); const pieces = jwt.split(".");
console.log("pieces", typeof pieces[1], pieces);
const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString()); const payload = JSON.parse(Buffer.from(pieces[1], "base64").toString());
const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64"); const authData = Buffer.from(payload["AuthenticationDataB64URL"], "base64");
const clientJSON = Buffer.from( const clientJSON = Buffer.from(
@@ -408,8 +446,8 @@ export default class Help extends Vue {
const challenge = clientData.challenge; const challenge = clientData.challenge;
const signatureB64URL = pieces[2]; const signatureB64URL = pieces[2];
const decoded = await verifyJwtWebCrypto( const decoded = await verifyJwtWebCrypto(
this.credIdHex as Base64URLString, this.credIdHex as string,
this.activeDid as string, did,
authData, authData,
challenge, challenge,
payload["ClientDataJSONB64URL"], payload["ClientDataJSONB64URL"],

View File

@@ -16,6 +16,8 @@ export default defineConfig({
srcDir: '.', srcDir: '.',
filename: 'sw_scripts-combined.js', filename: 'sw_scripts-combined.js',
manifest: { manifest: {
// This is used for the app name. It doesn't include a space, because iOS complains if i recall correctly.
// There is a name with spaces in the constants/app.js file for use internally.
name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name, short_name: process.env.TIME_SAFARI_APP_TITLE || require('./package.json').name,
// 192x192 and 512x512 are important for Chrome to show that it's installable // 192x192 and 512x512 are important for Chrome to show that it's installable