diff --git a/app/__test-fixtures__/databaseStorageFixtures.ts b/app/__test-fixtures__/databaseStorageFixtures.ts index 77f90e5486..ed0e0b9c29 100644 --- a/app/__test-fixtures__/databaseStorageFixtures.ts +++ b/app/__test-fixtures__/databaseStorageFixtures.ts @@ -51,6 +51,11 @@ export const facebookStampFixture: Stamp = { credential, }; +export const brightidStampFixture: Stamp = { + provider: "Brightid", + credential, +}; + export const passportFixture: Passport = { issuanceDate: new Date("2022-01-01"), expiryDate: new Date("2022-01-02"), diff --git a/app/__test-fixtures__/verifiableCredentialResults.ts b/app/__test-fixtures__/verifiableCredentialResults.ts index 4b3c5ed3b5..c1bbf485b8 100644 --- a/app/__test-fixtures__/verifiableCredentialResults.ts +++ b/app/__test-fixtures__/verifiableCredentialResults.ts @@ -63,3 +63,17 @@ export const SUCCESFUL_POH_RESULT: VerifiableCredentialRecord = { challenge: credential, credential: credential, }; + +export const SUCCESFUL_BRIGHTID_RESULT: VerifiableCredentialRecord = { + record: { + type: "Brightid", + address: "0xcF323CE817E25b4F784bC1e14c9A99A525fDC50f", + version: "0.0.0", + contextId: "somedata", + meets: "true", + }, + signature: + "0xbdbac10fdb0921e73df7575e47cbda484be550c36570bc146bed90c5dcb7435e64178cb263864f48af1ad6eeee1ee94c9a0794a3812ae861f8898a973233abea1c", + challenge: credential, + credential: credential, +}; diff --git a/app/__tests__/components/ProviderCards/BrightidCard.test.tsx b/app/__tests__/components/ProviderCards/BrightidCard.test.tsx new file mode 100644 index 0000000000..2d1e771ede --- /dev/null +++ b/app/__tests__/components/ProviderCards/BrightidCard.test.tsx @@ -0,0 +1,214 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor, waitForElementToBeRemoved } from "@testing-library/react"; +import { BrightidCard } from "../../../components/ProviderCards"; + +import { UserContext, UserContextState } from "../../../context/userContext"; +import { mockAddress, mockWallet } from "../../../__test-fixtures__/onboardHookValues"; +import { STAMP_PROVIDERS } from "../../../config/providers"; +import { brightidStampFixture } from "../../../__test-fixtures__/databaseStorageFixtures"; +import { SUCCESFUL_BRIGHTID_RESULT } from "../../../__test-fixtures__/verifiableCredentialResults"; +import { fetchVerifiableCredential } from "@dpopp/identity/dist/commonjs"; + +jest.mock("@dpopp/identity/dist/commonjs", () => ({ + fetchVerifiableCredential: jest.fn(), +})); +jest.mock("../../../utils/onboard.ts"); + +const mockHandleConnection = jest.fn(); +const mockCreatePassport = jest.fn(); +const handleAddStamp = jest.fn(); +const mockUserContext: UserContextState = { + userDid: "mockUserDid", + loggedIn: true, + passport: undefined, + isLoadingPassport: false, + allProvidersState: { + Brightid: { + providerSpec: STAMP_PROVIDERS.Brightid, + stamp: undefined, + }, + }, + handleAddStamp: handleAddStamp, + handleCreatePassport: mockCreatePassport, + handleConnection: mockHandleConnection, + address: mockAddress, + wallet: mockWallet, + signer: undefined, + walletLabel: mockWallet.label, +}; + +function setupFetchStub(valid: any) { + return function fetchStub(_url: any) { + return new Promise((resolve) => { + resolve({ + json: () => + Promise.resolve({ + valid, + }), + }); + }); + }; +} + +describe("when user has not verfied with BrightId Provider", () => { + it("should display a verify button", () => { + render( + + + + ); + + const initialVerifyButton = screen.queryByTestId("button-verify-brightid"); + + expect(initialVerifyButton).toBeInTheDocument(); + }); +}); + +describe("when user has verified with BrightId Provider", () => { + it("should display that a verified credential was returned", () => { + render( + + + + ); + + const brightidVerified = screen.queryByText(/Verified/); + + expect(brightidVerified).toBeInTheDocument(); + }); +}); + +describe("when the verify button is clicked", () => { + let originalFetch: any; + beforeEach(() => { + originalFetch = global.fetch; + global.fetch = jest.fn().mockImplementation(setupFetchStub(true)); + }); + + afterEach(() => { + global.fetch = originalFetch; + jest.clearAllMocks(); + }); + + describe("and when a successful BrightId result is returned", () => { + beforeEach(() => { + (fetchVerifiableCredential as jest.Mock).mockResolvedValue(SUCCESFUL_BRIGHTID_RESULT); + }); + + it("the modal displays the verify button", async () => { + render( + + + + ); + + const initialVerifyButton = screen.queryByTestId("button-verify-brightid"); + + fireEvent.click(initialVerifyButton!); + + const verifyModal = await screen.findByRole("dialog"); + const verifyModalButton = screen.getByTestId("modal-verify"); + + expect(verifyModal).toBeInTheDocument(); + + await waitFor(() => { + expect(verifyModalButton).toBeInTheDocument(); + }); + }); + + it("clicking verify adds the stamp", async () => { + render( + + + + ); + + const initialVerifyButton = screen.queryByTestId("button-verify-brightid"); + + // Click verify button on brightid card + fireEvent.click(initialVerifyButton!); + + // Wait to see the verify button on the modal + await waitFor(() => { + const verifyModalButton = screen.getByTestId("modal-verify"); + expect(verifyModalButton).toBeInTheDocument(); + }); + + const finalVerifyButton = screen.queryByRole("button", { + name: /Verify/, + }); + + // Click the verify button on modal + fireEvent.click(finalVerifyButton!); + + expect(handleAddStamp).toBeCalled(); + }); + + it("clicking cancel closes the modal and a stamp should not be added", async () => { + (fetchVerifiableCredential as jest.Mock).mockResolvedValue(SUCCESFUL_BRIGHTID_RESULT); + render( + + + + ); + + const initialVerifyButton = screen.queryByTestId("button-verify-brightid"); + + fireEvent.click(initialVerifyButton!); + + // Wait to see the cancel button on the modal + let modalCancelButton: HTMLElement | null = null; + await waitFor(() => { + modalCancelButton = screen.queryByRole("button", { + name: /Cancel/, + }); + expect(modalCancelButton).toBeInTheDocument(); + }); + + fireEvent.click(modalCancelButton!); + + expect(handleAddStamp).not.toBeCalled(); + + await waitForElementToBeRemoved(modalCancelButton); + expect(modalCancelButton).not.toBeInTheDocument(); + }); + }); + + describe("and when a failed Bright Id result is returned", () => { + it("modal displays steps to get sponsored", async () => { + global.fetch = jest.fn().mockImplementation(setupFetchStub(false)); + (fetchVerifiableCredential as jest.Mock).mockRejectedValue("ERROR"); + render( + + + + ); + + const initialVerifyButton = screen.queryByTestId("button-verify-brightid"); + + fireEvent.click(initialVerifyButton!); + + const verifyModal = await screen.findByRole("dialog"); + const triggerSponsorButton = screen.queryByTestId("button-sponsor-brightid"); + const verifyModalText = screen.getByText( + "A verifiable credential was not generated for your address. Follow the steps below to qualify:" + ); + + expect(verifyModal).toBeInTheDocument(); + await waitFor(() => { + expect(verifyModalText).toBeInTheDocument(); + }); + expect(triggerSponsorButton).toBeInTheDocument(); + }); + }); +}); diff --git a/app/__tests__/components/ProviderCards/EnsCard.test.tsx b/app/__tests__/components/ProviderCards/EnsCard.test.tsx index 28f04d3d9d..dc6ea190ce 100644 --- a/app/__tests__/components/ProviderCards/EnsCard.test.tsx +++ b/app/__tests__/components/ProviderCards/EnsCard.test.tsx @@ -18,6 +18,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, diff --git a/app/__tests__/components/ProviderCards/FacebookCard.test.tsx b/app/__tests__/components/ProviderCards/FacebookCard.test.tsx index f38c0cc897..8ca659be79 100644 --- a/app/__tests__/components/ProviderCards/FacebookCard.test.tsx +++ b/app/__tests__/components/ProviderCards/FacebookCard.test.tsx @@ -13,6 +13,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, diff --git a/app/__tests__/components/ProviderCards/GoogleCard.test.tsx b/app/__tests__/components/ProviderCards/GoogleCard.test.tsx index b57df4af29..74fa181976 100644 --- a/app/__tests__/components/ProviderCards/GoogleCard.test.tsx +++ b/app/__tests__/components/ProviderCards/GoogleCard.test.tsx @@ -13,6 +13,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, diff --git a/app/__tests__/components/ProviderCards/PoapCard.test.tsx b/app/__tests__/components/ProviderCards/PoapCard.test.tsx index 34b9e03aea..fcfec1604d 100644 --- a/app/__tests__/components/ProviderCards/PoapCard.test.tsx +++ b/app/__tests__/components/ProviderCards/PoapCard.test.tsx @@ -18,6 +18,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, diff --git a/app/__tests__/components/ProviderCards/PohCard.test.tsx b/app/__tests__/components/ProviderCards/PohCard.test.tsx index 7c2d77f569..aabc6edfca 100644 --- a/app/__tests__/components/ProviderCards/PohCard.test.tsx +++ b/app/__tests__/components/ProviderCards/PohCard.test.tsx @@ -18,6 +18,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, @@ -114,7 +115,7 @@ describe("when the verify button is clicked", () => { const initialVerifyButton = screen.queryByTestId("button-verify-poh"); - // Click verify button on ens card + // Click verify button on poh card fireEvent.click(initialVerifyButton!); // Wait to see the verify button on the modal diff --git a/app/__tests__/components/ProviderCards/TwitterCard.test.tsx b/app/__tests__/components/ProviderCards/TwitterCard.test.tsx index b1bca75364..bc662409d1 100644 --- a/app/__tests__/components/ProviderCards/TwitterCard.test.tsx +++ b/app/__tests__/components/ProviderCards/TwitterCard.test.tsx @@ -13,6 +13,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, diff --git a/app/__tests__/pages/Dashboard.test.tsx b/app/__tests__/pages/Dashboard.test.tsx index 7e8210d1f7..c706807d0a 100644 --- a/app/__tests__/pages/Dashboard.test.tsx +++ b/app/__tests__/pages/Dashboard.test.tsx @@ -12,6 +12,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: true, passport: undefined, isLoadingPassport: false, @@ -40,6 +41,10 @@ const mockUserContext: UserContextState = { providerSpec: STAMP_PROVIDERS.Facebook, stamp: undefined, }, + Brightid: { + providerSpec: STAMP_PROVIDERS.Brightid, + stamp: undefined, + }, }, handleAddStamp: handleAddStamp, handleCreatePassport: mockCreatePassport, diff --git a/app/__tests__/pages/Home.test.tsx b/app/__tests__/pages/Home.test.tsx index d283f09391..1b4081362d 100644 --- a/app/__tests__/pages/Home.test.tsx +++ b/app/__tests__/pages/Home.test.tsx @@ -12,6 +12,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: false, passport: undefined, isLoadingPassport: false, @@ -40,6 +41,10 @@ const mockUserContext: UserContextState = { providerSpec: STAMP_PROVIDERS.Facebook, stamp: undefined, }, + Brightid: { + providerSpec: STAMP_PROVIDERS.Brightid, + stamp: undefined, + }, }, handleAddStamp: handleAddStamp, handleCreatePassport: mockCreatePassport, diff --git a/app/__tests__/pages/index.test.tsx b/app/__tests__/pages/index.test.tsx index 4c8e735531..b2207faa5a 100644 --- a/app/__tests__/pages/index.test.tsx +++ b/app/__tests__/pages/index.test.tsx @@ -9,6 +9,7 @@ const mockHandleConnection = jest.fn(); const mockCreatePassport = jest.fn(); const handleAddStamp = jest.fn(); const mockUserContext: UserContextState = { + userDid: undefined, loggedIn: false, passport: undefined, isLoadingPassport: false, @@ -37,6 +38,10 @@ const mockUserContext: UserContextState = { providerSpec: STAMP_PROVIDERS.Facebook, stamp: undefined, }, + Brightid: { + providerSpec: STAMP_PROVIDERS.Brightid, + stamp: undefined, + }, }, handleAddStamp: handleAddStamp, handleCreatePassport: mockCreatePassport, diff --git a/app/components/CardList.tsx b/app/components/CardList.tsx index 66c5a1db82..61a90d3cc4 100644 --- a/app/components/CardList.tsx +++ b/app/components/CardList.tsx @@ -2,18 +2,19 @@ import React from "react"; // --- Identity Providers -import { GoogleCard, EnsCard, PohCard, TwitterCard, PoapCard, FacebookCard } from "./ProviderCards"; +import { GoogleCard, EnsCard, PohCard, TwitterCard, PoapCard, FacebookCard, BrightidCard } from "./ProviderCards"; export const CardList = (): JSX.Element => { return (
+ - - + - + +
); diff --git a/app/components/ProviderCards/BrightidCard.tsx b/app/components/ProviderCards/BrightidCard.tsx new file mode 100644 index 0000000000..471ba9cc3e --- /dev/null +++ b/app/components/ProviderCards/BrightidCard.tsx @@ -0,0 +1,224 @@ +// --- React Methods +import React, { useContext, useState } from "react"; + +// --- Identity tools +import { fetchVerifiableCredential } from "@dpopp/identity/dist/commonjs"; + +// --- pull context +import { UserContext } from "../../context/userContext"; + +// --- Verification step tools +import QRCode from "react-qr-code"; + +// --- import components +import { Card } from "../Card"; +import { VerifyModal } from "../VerifyModal"; +import { useDisclosure, Button, useToast } from "@chakra-ui/react"; + +// ---- Types +import { PROVIDER_ID, Stamp, BrightIdProcedureReturn } from "@dpopp/types"; +import { ProviderSpec } from "../../config/providers"; + +const iamUrl = process.env.NEXT_PUBLIC_DPOPP_IAM_URL || ""; + +const providerId: PROVIDER_ID = "Brightid"; + +type BrightIdProviderRecord = { + context?: string; + contextId?: string; + meets?: string; +}; + +export default function BrightIdCard(): JSX.Element { + const { address, signer, handleAddStamp, allProvidersState, userDid } = useContext(UserContext); + const { isOpen, onOpen, onClose } = useDisclosure(); + const [credentialResponse, SetCredentialResponse] = useState(undefined); + const [credentialResponseIsLoading, setCredentialResponseIsLoading] = useState(false); + const [brightIdVerification, SetBrightIdVerification] = useState(undefined); + const toast = useToast(); + + const handleFetchCredential = (): void => { + setCredentialResponseIsLoading(true); + fetchVerifiableCredential( + iamUrl, + { + type: "Brightid", + version: "0.0.0", + address: address || "", + proofs: { + did: userDid || "", + }, + }, + signer as { signMessage: (message: string) => Promise } + ) + .then((verified: { record: any; credential: any }): void => { + SetBrightIdVerification(verified.record); + SetCredentialResponse({ + provider: "Brightid", + credential: verified.credential, + }); + }) + .catch((e: any): void => {}) + .finally((): void => { + setCredentialResponseIsLoading(false); + }); + }; + + async function handleSponsorship(): Promise { + setCredentialResponseIsLoading(true); + const res = fetch(`${process.env.NEXT_PUBLIC_DPOPP_PROCEDURE_URL?.replace(/\/*?$/, "")}/brightid/sponsor`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + contextIdData: userDid, + }), + }); + const data = await (await res).json(); + if (data.response.result.status === "success") { + toast({ + title: "Success", + description: "Successfully triggered BrightID Sponsorship. Come back to Passport to Verify.", + status: "success", + duration: 9000, + isClosable: true, + }); + } else { + toast({ + title: "Failure", + description: "Failed to trigger BrightID Sponsorship", + status: "error", + duration: 9000, + isClosable: true, + }); + } + setCredentialResponseIsLoading(false); + } + + async function handleVerifyContextId(): Promise { + const res = await fetch( + `${process.env.NEXT_PUBLIC_DPOPP_PROCEDURE_URL?.replace(/\/*?$/, "")}/brightid/verifyContextId`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + contextIdData: userDid, + }), + } + ); + const result: BrightIdProcedureReturn = await res.json(); + return result.valid; + } + + const handleUserVerify = (): void => { + if (credentialResponse) { + handleAddStamp(credentialResponse); + } + onClose(); + }; + + // Widget displays steps to verify BrightID with Gitcoin + const brightIdSponsorshipWidget = ( +
+

BrightID

+ {userDid ? ( +
+
+

A verifiable credential was not generated for your address. Follow the steps below to qualify:

+
+
+

+ 1) Download the BrightID App on your mobile device{" "} + + Get Started + + . +

+
+
+ 2) Connect BrightID to Gitcoin by scanning this QR code from the BrightID app, or clicking{" "} + + here + {" "} + from your mobile device. +
+
+
+ +
+
+ + +
+
+ ) : ( +
Refresh Browser and Try Again
+ )} +
+ ); + + const issueCredentialWidget = ( + <> + + + {brightIdVerification + ? `Your BrightId has been verified on ${brightIdVerification.context}` + : brightIdSponsorshipWidget} + + } + isLoading={credentialResponseIsLoading} + /> + + ); + + return ( + + ); +} diff --git a/app/components/ProviderCards/index.tsx b/app/components/ProviderCards/index.tsx index 5a5fa11d52..78524d90c4 100644 --- a/app/components/ProviderCards/index.tsx +++ b/app/components/ProviderCards/index.tsx @@ -4,3 +4,4 @@ export { default as PohCard } from "./PohCard"; export { default as TwitterCard } from "./TwitterCard"; export { default as PoapCard } from "./PoapCard"; export { default as FacebookCard } from "./FacebookCard"; +export { default as BrightidCard } from "./BrightidCard"; diff --git a/app/config/providers.ts b/app/config/providers.ts index 645e5f08ee..bbc0ccc9e0 100644 --- a/app/config/providers.ts +++ b/app/config/providers.ts @@ -1,7 +1,7 @@ import { PROVIDER_ID } from "@dpopp/types"; export type ProviderSpec = { - icon: string | undefined; + icon?: string | undefined; name: string; description: string; }; @@ -41,4 +41,9 @@ export const STAMP_PROVIDERS: Readonly = { name: "Facebook", description: "Facebook name", }, + Brightid: { + icon: "./assets/brightidStampIcon.svg", + name: "Bright Id", + description: "Bright Id name", + }, }; diff --git a/app/context/userContext.tsx b/app/context/userContext.tsx index d37badcf40..c20346b5c8 100644 --- a/app/context/userContext.tsx +++ b/app/context/userContext.tsx @@ -53,6 +53,10 @@ const startingAllProvidersState: AllProvidersState = { providerSpec: STAMP_PROVIDERS.Facebook, stamp: undefined, }, + Brightid: { + providerSpec: STAMP_PROVIDERS.Brightid, + stamp: undefined, + }, }; export interface UserContextState { @@ -67,6 +71,7 @@ export interface UserContextState { wallet: WalletState | null; signer: JsonRpcSigner | undefined; walletLabel: string | undefined; + userDid: string | undefined; } const startingState: UserContextState = { loggedIn: false, @@ -80,6 +85,7 @@ const startingState: UserContextState = { wallet: null, signer: undefined, walletLabel: undefined, + userDid: undefined, }; // create our app context @@ -100,6 +106,7 @@ export const UserContextProvider = ({ children }: { children: any }) => { const [walletLabel, setWalletLabel] = useState(); const [address, setAddress] = useState(); const [signer, setSigner] = useState(); + const [userDid, setUserDid] = useState(); // Init onboard to enable hooks useEffect((): void => { @@ -168,6 +175,7 @@ export const UserContextProvider = ({ children }: { children: any }) => { ); setCeramicDatabase(ceramicDatabaseInstance); fetchPassport(ceramicDatabaseInstance); + setUserDid(viewerConnection.selfID.id); break; } case "failed": { @@ -213,6 +221,7 @@ export const UserContextProvider = ({ children }: { children: any }) => { // hydrate allProvidersState useEffect(() => { + console.log("VIEW passport ", passport); if (passport) { // set stamps into allProvidersState passport.stamps.forEach((stamp: Stamp) => { @@ -301,6 +310,7 @@ export const UserContextProvider = ({ children }: { children: any }) => { wallet, signer, walletLabel, + userDid, }), [loggedIn, address, passport, isLoadingPassport, signer, wallet, allProvidersState] ); @@ -318,6 +328,7 @@ export const UserContextProvider = ({ children }: { children: any }) => { wallet, signer, walletLabel, + userDid, }; return {children}; diff --git a/app/package.json b/app/package.json index fb1ae75569..14aee0c016 100644 --- a/app/package.json +++ b/app/package.json @@ -30,6 +30,7 @@ "broadcast-channel": "^4.11.0", "framer-motion": "^6.3.0", "next": "latest", + "node-fetch": "^3.2.4", "react": "^17.0.2", "react-dom": "^17.0.2", "react-google-login": "^5.2.2", @@ -41,9 +42,11 @@ "@glazed/did-datastore-model": "^0.2.0", "@glazed/types": "^0.2.0", "@next/eslint-plugin-next": "^12.1.5", + "@noble/ed25519": "^1.6.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^12.0.0", "@testing-library/user-event": "^14.1.1", + "@types/ed25519": "^0.0.0", "@types/jest": "^27.4.1", "@types/node": "17.0.4", "@types/react-dom": "^17.0.2", @@ -51,6 +54,9 @@ "@typescript-eslint/parser": "^5.20.0", "autoprefixer": "^10.4.0", "babel-jest": "^28.0.0", + "base64-js": "^1.5.1", + "ed25519": "^0.0.5", + "ed25519-keys": "^1.2.0", "eslint": "^8.13.0", "eslint-config-next": "^12.1.5", "eslint-config-prettier": "^8.5.0", @@ -64,6 +70,7 @@ "postcss": "^8.4.5", "prettier": "^2.6.2", "prettier-plugin-tailwindcss": "^0.1.1", + "react-qr-code": "^2.0.7", "tailwindcss": "^3.0.7", "ts-jest": "^27.1.4", "ts-node": "^10.8.0", diff --git a/app/public/assets/brightidStampIcon.svg b/app/public/assets/brightidStampIcon.svg index 04ede0143a..85dceb2390 100644 --- a/app/public/assets/brightidStampIcon.svg +++ b/app/public/assets/brightidStampIcon.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/iam/.env-example.env b/iam/.env-example.env index a5d8333c67..63c44d11a2 100644 --- a/iam/.env-example.env +++ b/iam/.env-example.env @@ -5,6 +5,7 @@ TWITTER_CLIENT_ID=MY-APP-ID-TWITTER TWITTER_CLIENT_SECRET=MY_APP_SECRET FACEBOOK_APP_ID=MY_APP_ID FACEBOOK_APP_SECRET=MY_APP_SECRET +BRIGHTID_PRIVATE_KEY=BRIGHTID_PRIVATE_KEY IAM_JWK='{"kty":"OKP","crv":"Ed25519","x":"yourIamKeyValues","d":"yourIamKeyValues"}' RPC_URL=https://eth-mainnet.alchemyapi.io/v2/ \ No newline at end of file diff --git a/iam/__tests__/brightid.test.ts b/iam/__tests__/brightid.test.ts new file mode 100644 index 0000000000..c67158a6c9 --- /dev/null +++ b/iam/__tests__/brightid.test.ts @@ -0,0 +1,168 @@ +// --- Test subject +import { BrightIdProvider } from "../src/providers/brightid"; +import { triggerBrightidSponsorship } from "../src/procedures/brightid"; +import { BrightIdVerificationResponse, BrightIdSponsorshipResponse } from "@dpopp/types"; +import { RequestPayload } from "@dpopp/types"; +import { verifyContextId, sponsor } from "brightid_sdk"; + +jest.mock("brightid_sdk", () => ({ + verifyContextId: jest.fn(), + sponsor: jest.fn(), +})); + +describe("Attempt BrightId", () => { + const did = "did:3:kjzl0cwe1jw1475ckc0ib6k44mm4sqegorxc1x23ppqh9lt9quso6yp7ayh2fae"; + const urlEncodedDid = "did%3A3%3Akjzl0cwe1jw1475ckc0ib6k44mm4sqegorxc1x23ppqh9lt9quso6yp7ayh2fae"; + const nonUniqueResponse: BrightIdVerificationResponse = { + unique: false, + app: "Gitcoin", + context: "Gitcoin", + contextIds: ["sampleContextId"], + }; + + const validVerificationResponse: BrightIdVerificationResponse = { + unique: true, + app: "Gitcoin", + context: "Gitcoin", + contextIds: ["sampleContextId"], + }; + + let invalidVerificationResponse: BrightIdVerificationResponse = { + status: 400, + statusText: "Not Found", + data: { + error: true, + errorNum: 2, + errorMessage: "Not Found", + contextIds: ["sampleContextId"], + code: 400, + }, + }; + + const validSponsorshipResponse: BrightIdSponsorshipResponse = { + status: "success", + statusReason: "successfulStatusReason", + }; + + let invalidSponsorshipResponse: BrightIdSponsorshipResponse = { + status: 404, + statusText: "Not Found", + data: { + error: true, + errorNum: 12, + errorMessage: "Passport app is not found.", + code: 404, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Handles Verification", () => { + it("valid BrightId did as contextId verification attempt, returns valid true, verifies if user has Meet status and verified contextId", async () => { + (verifyContextId as jest.Mock).mockResolvedValue(validVerificationResponse); + + const result = await new BrightIdProvider().verify({ + proofs: { + did, + }, + } as unknown as RequestPayload); + + expect(verifyContextId).toBeCalledTimes(1); + expect(verifyContextId).toBeCalledWith("Gitcoin", urlEncodedDid); + expect(result).toMatchObject({ + valid: true, + record: { + contextId: "sampleContextId", + meets: "true", + }, + }); + }); + + it("invalid BrightId did as contextId verification attempt, returns valid false and record undefined", async () => { + (verifyContextId as jest.Mock).mockResolvedValue(invalidVerificationResponse); + + const result = await new BrightIdProvider().verify({ + proofs: { + did, + }, + } as unknown as RequestPayload); + + expect(verifyContextId).toBeCalledTimes(1); + expect(result).toMatchObject({ + valid: false, + record: undefined, + }); + }); + + it("thrown error from BrightId did as contextId verification attempt, returns valid false", async () => { + (verifyContextId as jest.Mock).mockRejectedValue("Thrown Error"); + + const result = await new BrightIdProvider().verify({ + proofs: { + did, + }, + } as unknown as RequestPayload); + + expect(verifyContextId).toBeCalledTimes(1); + expect(result).toMatchObject({ + valid: false, + }); + }); + + it("user is sponsored but did not attend a connection party, returns valid false and record undefined", async () => { + (verifyContextId as jest.Mock).mockResolvedValue(nonUniqueResponse); + + const result = await new BrightIdProvider().verify({ + proofs: { + did, + }, + } as unknown as RequestPayload); + + expect(verifyContextId).toBeCalledTimes(1); + expect(result).toMatchObject({ + valid: false, + record: undefined, + }); + }); + }); + + describe("Handles Sponsorship", () => { + it("successful attempt", async () => { + (sponsor as jest.Mock).mockResolvedValue(validSponsorshipResponse); + const result = await triggerBrightidSponsorship(did); + + expect(sponsor).toBeCalledTimes(1); + expect(sponsor).toBeCalledWith(process.env.BRIGHTID_PRIVATE_KEY, "Gitcoin", urlEncodedDid); + expect(result).toMatchObject({ + valid: true, + result: validSponsorshipResponse, + }); + }); + + it("unsuccessful attempt", async () => { + (sponsor as jest.Mock).mockResolvedValue(invalidSponsorshipResponse); + const result = await triggerBrightidSponsorship(did); + + expect(sponsor).toBeCalledTimes(1); + expect(sponsor).toBeCalledWith(process.env.BRIGHTID_PRIVATE_KEY, "Gitcoin", urlEncodedDid); + expect(result).toMatchObject({ + valid: false, + result: invalidSponsorshipResponse, + }); + }); + + it("error thrown from an unsuccessful attempt", async () => { + (sponsor as jest.Mock).mockRejectedValue("Thrown Error"); + const result = await triggerBrightidSponsorship(did); + + expect(sponsor).toBeCalledTimes(1); + expect(sponsor).toBeCalledWith(process.env.BRIGHTID_PRIVATE_KEY, "Gitcoin", urlEncodedDid); + expect(result).toMatchObject({ + valid: false, + error: "Thrown Error", + }); + }); + }); +}); diff --git a/iam/package.json b/iam/package.json index 8f0de44a05..8684921931 100644 --- a/iam/package.json +++ b/iam/package.json @@ -21,6 +21,7 @@ "@dpopp/types": "^0.0.1", "@spruceid/didkit-wasm-node": "^0.2.1", "axios": "^0.27.2", + "brightid_sdk": "^1.0.1", "cors": "^2.8.5", "dotenv": "^16.0.0", "ethers": "^5.0.32", @@ -29,17 +30,24 @@ "luxon": "^2.4.0", "tslint": "^6.1.3", "twitter-api-sdk": "1.0.6", - "typescript": "~4.6.3" + "typescript": "~4.6.3", + "uuid": "^8.3.2" }, "devDependencies": { + "@ethersproject/base64": "^5.6.1", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", "@types/luxon": "^2.3.2", "@types/node": "16", "@types/node-fetch": "latest", "@types/supertest": "^2.0.12", + "@types/uuid": "^8.3.4", "@types/webpack-env": "^1.16.3", "jest": "^27.5.1", - "supertest": "^6.2.2" + "supertest": "^6.2.2", + "uuid": "^8.3.2" + }, + "resolutions": { + "leveldown": "6.1.1" } } diff --git a/iam/src/index.ts b/iam/src/index.ts index ae914fb842..f00de27929 100644 --- a/iam/src/index.ts +++ b/iam/src/index.ts @@ -37,6 +37,7 @@ import { EnsProvider } from "./providers/ens"; import { PohProvider } from "./providers/poh"; import { POAPProvider } from "./providers/poap"; import { FacebookProvider } from "./providers/facebook"; +import { BrightIdProvider } from "./providers/brightid"; // Initiate providers - new Providers should be registered in this array... const providers = new Providers([ @@ -48,6 +49,7 @@ const providers = new Providers([ new PohProvider(), new POAPProvider(), new FacebookProvider(), + new BrightIdProvider(), ]); // create the app and run on port diff --git a/iam/src/procedures/brightid.ts b/iam/src/procedures/brightid.ts new file mode 100644 index 0000000000..ae4c2c7c47 --- /dev/null +++ b/iam/src/procedures/brightid.ts @@ -0,0 +1,41 @@ +import { verifyContextId, sponsor } from "brightid_sdk"; +import { BrightIdProcedureReturn, BrightIdVerificationResponse, BrightIdSponsorshipResponse } from "@dpopp/types"; + +// --- app name for Bright Id App +const CONTEXT = "Gitcoin"; + +export const verifyBrightidContextId = async (contextIdData: string): Promise => { + const contextId = encodeURIComponent(contextIdData); + + try { + const verifyContextIdResult: BrightIdVerificationResponse = (await verifyContextId( + CONTEXT, + contextId + )) as BrightIdVerificationResponse; + + if ("contextIds" in verifyContextIdResult) { + return { valid: true, result: verifyContextIdResult }; + } + return { valid: false, result: verifyContextIdResult }; + } catch (err: unknown) { + return { valid: false, error: err as string }; + } +}; + +export const triggerBrightidSponsorship = async (contextIdData: string): Promise => { + const contextId = encodeURIComponent(contextIdData); + + try { + const sponsorResult: BrightIdSponsorshipResponse = (await sponsor( + process.env.BRIGHTID_PRIVATE_KEY, + CONTEXT, + contextId + )) as BrightIdSponsorshipResponse; + if (sponsorResult.status === "success") { + return { valid: true, result: sponsorResult }; + } + return { valid: false, result: sponsorResult }; + } catch (err: unknown) { + return { valid: false, error: err as string }; + } +}; diff --git a/iam/src/procedures/index.ts b/iam/src/procedures/index.ts index e6a4597177..23b46a65d5 100644 --- a/iam/src/procedures/index.ts +++ b/iam/src/procedures/index.ts @@ -2,6 +2,7 @@ import { Request, Response, Router } from "express"; import { generateAuthURL, getSessionKey, initClient } from "./twitterOauth"; +import { triggerBrightidSponsorship, verifyBrightidContextId } from "./brightid"; export const router = Router(); @@ -9,6 +10,10 @@ export type GenerateTwitterAuthUrlRequestBody = { callback: string; }; +export type GenerateBrightidBody = { + contextIdData: string; +}; + router.post("/twitter/generateAuthUrl", (req: Request, res: Response): void => { const { callback } = req.body as GenerateTwitterAuthUrlRequestBody; if (callback) { @@ -25,3 +30,25 @@ router.post("/twitter/generateAuthUrl", (req: Request, res: Response): void => { res.status(400); } }); + +router.post("/brightid/sponsor", (req: Request, res: Response): void => { + const { contextIdData } = req.body as GenerateBrightidBody; + if (contextIdData) { + return void triggerBrightidSponsorship(contextIdData).then((response) => { + return res.status(200).send({ response }); + }); + } else { + res.status(400); + } +}); + +router.post("/brightid/verifyContextId", (req: Request, res: Response): void => { + const { contextIdData } = req.body as GenerateBrightidBody; + if (contextIdData) { + return void verifyBrightidContextId(contextIdData).then((response) => { + return res.status(200).send({ response }); + }); + } else { + res.status(400); + } +}); diff --git a/iam/src/providers/brightid.ts b/iam/src/providers/brightid.ts new file mode 100644 index 0000000000..48bc1f1816 --- /dev/null +++ b/iam/src/providers/brightid.ts @@ -0,0 +1,47 @@ +// ----- Types +import type { Provider, ProviderOptions } from "../types"; +import type { RequestPayload, VerifiedPayload, BrightIdVerificationResponse } from "@dpopp/types"; + +// --- verifyMethod in providers +import { verifyBrightidContextId } from "../procedures/brightid"; + +// Request a verifiable credential from brightid +export class BrightIdProvider implements Provider { + // Give the provider a type so that we can select it with a payload + type = "Brightid"; + // Options can be set here and/or via the constructor + _options = {}; + + // construct the provider instance with supplied options + constructor(options: ProviderOptions = {}) { + this._options = { ...this._options, ...options }; + } + + async verify(payload: RequestPayload): Promise { + try { + const did = payload.proofs?.did; + + const responseData = await verifyBrightidContextId(did); + const formattedData: BrightIdVerificationResponse = responseData?.result as BrightIdVerificationResponse; + + // Unique is true if the user obtained "Meets" verification by attending a connection party + const isUnique = "unique" in formattedData && formattedData.unique === true; + const firstContextId = + "contextIds" in formattedData && formattedData.contextIds.length > 0 && formattedData.contextIds[0]; + const valid: boolean = firstContextId && isUnique; + + return { + valid, + record: valid + ? { + context: "context" in formattedData && formattedData.context, + contextId: firstContextId, + meets: JSON.stringify(isUnique), + } + : undefined, + }; + } catch (e) { + return { valid: false }; + } + } +} diff --git a/infra/production/index.ts b/infra/production/index.ts index 2ca97fd3f2..739239bfbc 100644 --- a/infra/production/index.ts +++ b/infra/production/index.ts @@ -188,6 +188,10 @@ const service = new awsx.ecs.FargateService("dpopp-iam", { name: "FACEBOOK_APP_SECRET", valueFrom: `${IAM_SERVER_SSM_ARN}:FACEBOOK_APP_SECRET::`, }, + { + name: "BRIGHTID_PRIVATE_KEY", + valueFrom: `${IAM_SERVER_SSM_ARN}:BRIGHTID_PRIVATE_KEY::`, + }, ], }, }, diff --git a/infra/review/index.ts b/infra/review/index.ts index 81f2e1a2ca..41b970e169 100644 --- a/infra/review/index.ts +++ b/infra/review/index.ts @@ -187,6 +187,10 @@ const service = new awsx.ecs.FargateService("dpopp-iam", { name: "FACEBOOK_APP_SECRET", valueFrom: `${IAM_SERVER_SSM_ARN}:FACEBOOK_APP_SECRET::`, }, + { + name: "BRIGHTID_PRIVATE_KEY", + valueFrom: `${IAM_SERVER_SSM_ARN}:BRIGHTID_PRIVATE_KEY::`, + }, ], }, }, diff --git a/infra/staging/index.ts b/infra/staging/index.ts index 95b7acae87..d4d5c662ba 100644 --- a/infra/staging/index.ts +++ b/infra/staging/index.ts @@ -191,6 +191,10 @@ const service = new awsx.ecs.FargateService("dpopp-iam", { name: "FACEBOOK_APP_SECRET", valueFrom: `${IAM_SERVER_SSM_ARN}:FACEBOOK_APP_SECRET::`, }, + { + name: "BRIGHTID_PRIVATE_KEY", + valueFrom: `${IAM_SERVER_SSM_ARN}:BRIGHTID_PRIVATE_KEY::`, + }, ], }, }, diff --git a/types/src/brightid.d.ts b/types/src/brightid.d.ts new file mode 100644 index 0000000000..9ee76235b4 --- /dev/null +++ b/types/src/brightid.d.ts @@ -0,0 +1,33 @@ +export type FailResponse = { + status?: number; + statusText?: string; + data?: { + error: boolean; + errorNum: number; + errorMessage: string; + contextIds?: string[]; + code: number; + }; +}; + +export type SponsorshipSuccessResponse = { + status: string; + statusReason: string; +}; + +export type VerificationSuccessResponse = { + unique?: boolean; + app?: string; + context?: string; + contextIds?: string[]; +}; + +export type BrightIdVerificationResponse = FailResponse | VerificationSuccessResponse; + +export type BrightIdSponsorshipResponse = FailResponse | SponsorshipSuccessResponse; + +export type BrightIdProcedureReturn = { + valid: boolean; + result?: BrightIdSponsorshipResponse | BrightIdVerificationResponse; + error?: string; +}; diff --git a/types/src/index.d.ts b/types/src/index.d.ts index 8fb9a5a60f..50adb63994 100644 --- a/types/src/index.d.ts +++ b/types/src/index.d.ts @@ -1,3 +1,6 @@ +// BrightId Shared Types +export { BrightIdProcedureReturn, BrightIdVerificationResponse, BrightIdSponsorshipResponse } from "./brightid"; + // Typing for required parts of DIDKit export type DIDKitLib = { verifyCredential: (vc: string, proofOptions: string) => Promise; @@ -119,4 +122,4 @@ export type Passport = { // Passport DID export type DID = string; -export type PROVIDER_ID = "Google" | "Ens" | "Poh" | "Twitter" | "POAP" | "Facebook"; +export type PROVIDER_ID = "Google" | "Ens" | "Poh" | "Twitter" | "POAP" | "Facebook" | "Brightid"; diff --git a/yarn.lock b/yarn.lock index d2a8a2f1b5..2691130756 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3893,9 +3893,9 @@ resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz#a350caf42975e7197b24b495b8d764eec7e6a36e" integrity sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA== -"@noble/ed25519@^1.5.1": +"@noble/ed25519@^1.5.1", "@noble/ed25519@^1.6.0": version "1.6.0" - resolved "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.6.0.tgz" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.6.0.tgz#b55f7c9e532b478bf1d7c4f609e1f3a37850b583" integrity sha512-UKju89WV37IUALIMfKhKW3psO8AqmrE/GvH6QbPKjzolQ98zM7WmGUeY+xdIgSf5tqPFf75ZCYMgym6E9Jsw3Q== "@noble/secp256k1@^1.3.0": @@ -4811,6 +4811,13 @@ dependencies: "@types/ms" "*" +"@types/ed25519@^0.0.0": + version "0.0.0" + resolved "https://registry.npmjs.org/@types/ed25519/-/ed25519-0.0.0.tgz" + integrity sha512-i/NlbE4RSlXWckmNaRTPvNwM0MnirlP0FtCKhsjV+PExR9JtwDd8GxwOD7k9T1APxDpIG83aMZBtv1uy/AHJpg== + dependencies: + "@types/node" "*" + "@types/express-serve-static-core@^4.17.18": version "4.17.28" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz" @@ -4910,7 +4917,7 @@ "@types/luxon@^2.3.2": version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-2.3.2.tgz#8a3f2cdd4858ce698b56cd8597d9243b8e9d3c65" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.3.2.tgz" integrity sha512-WOehptuhKIXukSUUkRgGbj2c997Uv/iUgYgII8U7XLJqq9W2oF0kQ6frEznRQbdurioz+L/cdaIm4GutTQfgmA== "@types/mime@^1": @@ -5082,6 +5089,11 @@ dependencies: "@types/jest" "*" +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/warning@^3.0.0": version "3.0.0" resolved "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz" @@ -5973,6 +5985,13 @@ axe-core@^4.3.5: resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== +axios@^0.21.0: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + axios@^0.24.0: version "0.24.0" resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz" @@ -5989,7 +6008,7 @@ axios@^0.26.1: axios@^0.27.2: version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== dependencies: follow-redirects "^1.14.9" @@ -6149,7 +6168,7 @@ base32-encode@1.2.0: dependencies: to-data-view "^1.1.0" -base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1: +base64-js@^1.0.2, base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -6216,7 +6235,7 @@ bind-decorator@^1.0.11: resolved "https://registry.npmjs.org/bind-decorator/-/bind-decorator-1.0.11.tgz" integrity sha1-5BvAah9l3ZzsR2yRxdrzl4SIJS8= -bindings@^1.3.0: +bindings@^1.3.0, bindings@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -6424,6 +6443,16 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brightid_sdk@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/brightid_sdk/-/brightid_sdk-1.0.1.tgz#e848865456f2db8ba49c2156483814e44a10ef9f" + integrity sha512-3tdprkXj/odL5BpQEHnWSLdwAxmMvs8AkRk7rF6UGnAoQTZjR4FEPNkxBAuPF2BbDoqKw2ebQ7P2LPhPGsahdA== + dependencies: + axios "^0.21.0" + base64-js "^1.5.1" + fast-json-stable-stringify "^2.1.0" + tweetnacl "^1.0.3" + broadcast-channel@^3.4.1: version "3.7.0" resolved "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz" @@ -7535,6 +7564,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" + integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz" @@ -8007,6 +8041,21 @@ ecurve@1.0.5: dependencies: bigi "^1.1.0" +ed25519-keys@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/ed25519-keys/-/ed25519-keys-1.2.0.tgz#6e5e4dcd54c41855937452b9e97cd10865b6ca61" + integrity sha512-U+ZbYlQcDQrKZmN1uO0A2ZXnmaE/1UjSn55T3gBy1qWOAm6HITvWTd05akm2DNCR5JHgvdjSQgeULxOmW3n77g== + dependencies: + elliptic "^6.5.4" + +ed25519@^0.0.5: + version "0.0.5" + resolved "https://registry.npmjs.org/ed25519/-/ed25519-0.0.5.tgz" + integrity sha512-bg8uNetUVrkTyAVsnCUUmC/we1CFB7DYa59rwEUHut26SISXN2o319HzIt0r7ncqbtjVi0hn9r/A0epUmMNulQ== + dependencies: + bindings "^1.5.0" + nan "^2.14.1" + ed2curve@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/ed2curve/-/ed2curve-0.3.0.tgz" @@ -9086,6 +9135,14 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.5.tgz#0077bf5f3fcdbd9d75a0b5362f77dbb743489863" + integrity sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + figures@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" @@ -9208,6 +9265,11 @@ focus-lock@^0.9.1: dependencies: tslib "^2.0.3" +follow-redirects@^1.14.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + follow-redirects@^1.14.4, follow-redirects@^1.14.8: version "1.14.9" resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz" @@ -9215,7 +9277,7 @@ follow-redirects@^1.14.4, follow-redirects@^1.14.8: follow-redirects@^1.14.9: version "1.15.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.0.tgz#06441868281c86d0dda4ad8bdaead2d02dca89d4" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz" integrity sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ== forever-agent@~0.6.1: @@ -9250,6 +9312,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + formidable@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz" @@ -12679,7 +12748,7 @@ ltgt@^2.1.2, ltgt@^2.2.1: luxon@^2.4.0: version "2.4.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.4.0.tgz#9435806545bb32d4234dab766ab8a3d54847a765" + resolved "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz" integrity sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA== lz-string@^1.4.4: @@ -13241,6 +13310,11 @@ nan@^2.13.2: resolved "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nan@^2.14.1: + version "2.16.0" + resolved "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz" + integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA== + nano-time@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz" @@ -13373,6 +13447,11 @@ node-addon-api@^2.0.0: resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" @@ -13380,6 +13459,15 @@ node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.4.tgz#3fbca2d8838111048232de54cb532bd3cf134947" + integrity sha512-WvYJRN7mMyOLurFR2YpysQGuwYrJN+qrrpHjJDuKMcSPdfFccRUla/kng2mz6HWSBxJcqPbvatS6Gb4RhOzCJw== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + "node-fetch@https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz": version "2.6.7" resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz#1b5d62978f2ed07b99444f64f0df39f960a6d34d" @@ -14487,6 +14575,11 @@ q@^1.5.1: resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qr.js@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" + integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8= + qrcode@1.4.4: version "1.4.4" resolved "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz" @@ -14680,6 +14773,14 @@ react-native-fetch-api@^2.0.0: dependencies: p-defer "^3.0.0" +react-qr-code@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.7.tgz#508304e031e82426a044f5e9490aca87d7c3de38" + integrity sha512-NpknL80p7dWbLdHfBSIxQIqLCu3+kBlyzYD692rO0UnVwfCSziIxc1xn/p3JhPEv1RV1lRD8j0w+hR3L7tawTQ== + dependencies: + prop-types "^15.7.2" + qr.js "0.0.0" + react-query@^3.35.0: version "3.38.1" resolved "https://registry.npmjs.org/react-query/-/react-query-3.38.1.tgz" @@ -15434,7 +15535,7 @@ socks-proxy-agent@^5.0.0: debug "4" socks "^2.3.3" -socks-proxy-agent@^6.0.0, socks-proxy-agent@^6.1.1: +socks-proxy-agent@^6.0.0: version "6.2.0" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.0.tgz#f6b5229cc0cbd6f2f202d9695f09d871e951c85e" integrity sha512-wWqJhjb32Q6GsrUqzuFkukxb/zzide5quXYcMVpIjxalDBBYy2nqKCFQ/9+Ie4dvOYSQdOk3hUlZSdzZOd3zMQ== @@ -15443,6 +15544,15 @@ socks-proxy-agent@^6.0.0, socks-proxy-agent@^6.1.1: debug "^4.3.3" socks "^2.6.2" +socks-proxy-agent@^6.1.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + socks@^2.3.3, socks@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz" @@ -16645,6 +16755,11 @@ uuid@^3.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache-lib@^3.0.0, v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" @@ -16764,6 +16879,11 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"