Skip to content

Commit

Permalink
feat: update credo, add mdoc (#192)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra authored Oct 29, 2024
1 parent 0502e03 commit 52497eb
Show file tree
Hide file tree
Showing 28 changed files with 3,571 additions and 3,890 deletions.
930 changes: 0 additions & 930 deletions .yarn/patches/@sphereon-pex-npm-3.3.2-144d9252ec.patch

This file was deleted.

28 changes: 0 additions & 28 deletions .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs

This file was deleted.

873 changes: 0 additions & 873 deletions .yarn/releases/yarn-3.5.0.cjs

This file was deleted.

2 changes: 1 addition & 1 deletion apps/easypid/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"ascAppId": "6636489314"
},
"android": {
"track": "alpha"
"track": "internal"
}
},
"preview": {
Expand Down
2 changes: 1 addition & 1 deletion apps/easypid/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "animo-easypid",
"version": "1.6.3",
"version": "1.7.0",
"main": "index.ts",
"private": true,
"scripts": {
Expand Down
8 changes: 7 additions & 1 deletion apps/easypid/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ export const bdrPidIssuerCertificate =
'MIICeTCCAiCgAwIBAgIUB5E9QVZtmUYcDtCjKB/H3VQv72gwCgYIKoZIzj0EAwIwgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xHTAbBgNVBAoMFEJ1bmRlc2RydWNrZXJlaSBHbWJIMREwDwYDVQQLDAhUIENTIElERTE2MDQGA1UEAwwtU1BSSU5EIEZ1bmtlIEVVREkgV2FsbGV0IFByb3RvdHlwZSBJc3N1aW5nIENBMB4XDTI0MDUzMTA2NDgwOVoXDTM0MDUyOTA2NDgwOVowgYgxCzAJBgNVBAYTAkRFMQ8wDQYDVQQHDAZCZXJsaW4xHTAbBgNVBAoMFEJ1bmRlc2RydWNrZXJlaSBHbWJIMREwDwYDVQQLDAhUIENTIElERTE2MDQGA1UEAwwtU1BSSU5EIEZ1bmtlIEVVREkgV2FsbGV0IFByb3RvdHlwZSBJc3N1aW5nIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYGzdwFDnc7+Kn5ibAvCOM8ke77VQxqfMcwZL8IaIA+WCROcCfmY/giH92qMru5p/kyOivE0RC/IbdMONvDoUyaNmMGQwHQYDVR0OBBYEFNRWGMCJOOgOWIQYyXZiv6u7xZC+MB8GA1UdIwQYMBaAFNRWGMCJOOgOWIQYyXZiv6u7xZC+MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA0cAMEQCIGEm7wkZKHt/atb4MdFnXW6yrnwMUT2u136gdtl10Y6hAiBuTFqvVYth1rbxzCP0xWZHmQK9kVyxn8GPfX27EIzzsw=='

// https://funke.animo.id
const animoFunkeRelyingPartyCertificate =
// Old one, keeping it in to not break things
const oldAnimoFunkeRelyingPartyCertificate =
'MIH6MIGhoAMCAQICEDlbxpcN1V1PRbmc2TtPjNQwCgYIKoZIzj0EAwIwADAeFw03MDAxMDEwMDAwMDBaFw0yNTExMjIwODIyMTJaMAAwOTATBgcqhkjOPQIBBggqhkjOPQMBBwMiAALcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xaMdMBswGQYDVR0RBBIwEIIOZnVua2UuYW5pbW8uaWQwCgYIKoZIzj0EAwIDSAAwRQIhAIFd2jlrZAzLTLsXdUE7O+CRuxuzk04lGo1eVYIbgT8iAiAQhR/FonhoLLTFjU/3tn5rPyB2DaOl3W18W5ugLWHjhQ=='

// Includes C=NL (required for the mdoc verification)
const animoFunkeRelyingPartyCertificate =
'MIIBEzCBu6ADAgECAhBwnaR5jboQ4R7Ne/k+sfhCMAoGCCqGSM49BAMCMA0xCzAJBgNVBAYTAk5MMB4XDTcwMDEwMTAwMDAwMFoXDTI1MTEyMjA4MjIxMlowDTELMAkGA1UEBhMCTkwwOTATBgcqhkjOPQIBBggqhkjOPQMBBwMiAALcD1XzKepFxWMAOqV+ln1fybBt7DRO5CV0f9A6mRp2xaMdMBswGQYDVR0RBBIwEIIOZnVua2UuYW5pbW8uaWQwCgYIKoZIzj0EAwIDRwAwRAIgPM5ITfUD6VWLgRm8Eu1gw53Of+SUdjS+yRClR68m//4CIDxnng7NnJyGnpsKDuUqSIl/A0rRQCwTLBZw9Hx3MZnZ'

const ubiqueRootCertificate =
'MIIBZjCCAQygAwIBAgIGAZGJt173MAoGCCqGSM49BAMCMB8xHTAbBgNVBAMMFGh0dHBzOi8vYXV0aG9yaXR5LmNoMB4XDTI0MDgyNTEzMjYyMVoXDTI1MDgyNTEzMjYyMVowHzEdMBsGA1UEAwwUaHR0cHM6Ly9hdXRob3JpdHkuY2gwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAScIjAmHrkp3TC6bisgaqmszbKkpY0iGTdHF2rcRemJCV+ikotDt7G+ApwG0m6fxt8aBJHeJ2mssLvZBmZj5LtWozQwMjAfBgNVHREEGDAWghRodHRwczovL2F1dGhvcml0eS5jaDAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIQCpQsxyQx/5knqhGnDCiAo6MpQmTCd7vA9WehF4/1P8/QIgEnAtFVTP1uThuTEna1RD4Ji35+z1h8pDoMyLPd3Uaig='

export const trustedX509Certificates = [
bdrPidIssuerCertificate,
animoFunkeRelyingPartyCertificate,
ubiqueRootCertificate,
oldAnimoFunkeRelyingPartyCertificate,
]

// https://gitlab.opencode.de/bmi/eudi-wallet/eidas-2.0-architekturkonzept/-/blob/main/architecture-proposal.md#pid-contents
Expand Down
9 changes: 2 additions & 7 deletions apps/easypid/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sendCommand } from '@animo-id/expo-ausweis-sdk'
import type { SdJwtVcHeader } from '@credo-ts/core'
import { /*MdocRecord, */ TypedArrayEncoder, utils } from '@credo-ts/core'
import { MdocRecord, utils } from '@credo-ts/core'
import { type AppAgent, initializeAppAgent, useSecureUnlock } from '@easypid/agent'
import { deviceKeyPair } from '@easypid/storage/pidPin'
import { PinPossiblyReusedError, ReceivePidUseCaseBPrimeFlow } from '@easypid/use-cases/ReceivePidUseCaseBPrimeFlow'
Expand Down Expand Up @@ -711,12 +711,7 @@ export function OnboardingContextProvider({
entityName: issuerName,
credentialIds: [credential.id],
})
} /* else if (credential instanceof MdocRecord) {
await storeCredential(secureUnlock.context.agent, credential)
// NOTE: we don't set the userName here as we always get SD-JWT VC and MODC at the same time currently
// so it should be set
} */
}
}

setCurrentStepName('id-card-complete')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SdJwtVcRecord, W3cCredentialRecord } from '@package/agent'
import type { MdocRecord, SdJwtVcRecord, W3cCredentialRecord } from '@package/agent'

import { utils } from '@credo-ts/core'
import { useAppAgent } from '@easypid/agent'
Expand Down Expand Up @@ -30,7 +30,7 @@ export function FunkeOpenIdCredentialNotificationScreen() {
const { params } = useParams()
const pushToWallet = usePushToWallet()

const [credentialRecord, setCredentialRecord] = useState<W3cCredentialRecord | SdJwtVcRecord>()
const [credentialRecord, setCredentialRecord] = useState<W3cCredentialRecord | SdJwtVcRecord | MdocRecord>()
const [errorReason, setErrorReason] = useState<string>()
const [isAccepted, setIsAccepted] = useState(false)
const [isStoring, setIsStoring] = useState(false)
Expand All @@ -48,10 +48,7 @@ export function FunkeOpenIdCredentialNotificationScreen() {
accessToken: tokenResponse,
})

// if (credentialRecord.type === 'MdocRecord') {
// throw new Error('mdoc not supported')
// }
setCredentialRecord(credentialRecord as W3cCredentialRecord | SdJwtVcRecord)
setCredentialRecord(credentialRecord as W3cCredentialRecord | SdJwtVcRecord | MdocRecord)
} catch (e: unknown) {
agent.config.logger.error(`Couldn't receive credential from OpenID4VCI offer`, {
error: e,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { type SdJwtVcRecord, type W3cCredentialRecord, getCredentialForDisplay } from '@package/agent'
import { type MdocRecord, type SdJwtVcRecord, type W3cCredentialRecord, getCredentialForDisplay } from '@package/agent'
import {
AnimatedStack,
Button,
Heading,
HeroIcons,
Loader,
Paragraph,
ScrollView,
Spacer,
Stack,
XStack,
YStack,
useInitialRender,
Expand All @@ -35,7 +33,7 @@ import {
} from 'react-native-reanimated'

interface OfferCredentialSlideProps {
credentialRecord?: SdJwtVcRecord | W3cCredentialRecord
credentialRecord?: SdJwtVcRecord | W3cCredentialRecord | MdocRecord
isStoring: boolean
isAccepted: boolean
onAccept: () => Promise<void>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function FunkeOpenIdPresentationNotificationScreen() {
? getPidAttributesForDisplay(
credential.disclosedPayload ?? {},
credential.metadata ?? ({} as CredentialMetadata),
credential.claimFormat as ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
credential.claimFormat as ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
)
: credential.disclosedPayload

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export function RequestedAttributesSection({ submission }: RequestedAttributesSe
if (credential.metadata?.type === pidCredential?.type) {
const disclosedAttributes = getPidDisclosedAttributeNames(
credential?.disclosedPayload ?? {},
credential?.claimFormat as ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
credential?.claimFormat as ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
)

const disclosedPayload = getPidAttributesForDisplay(
credential?.disclosedPayload ?? {},
credential?.metadata ?? ({} as CredentialMetadata),
credential?.claimFormat as ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
credential?.claimFormat as ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
)

return (
Expand Down
9 changes: 4 additions & 5 deletions apps/easypid/src/hooks/usePidCredential.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useSeedCredentialPidData } from '@easypid/storage'
import { type CredentialMetadata, useCredentialsForDisplay } from '@package/agent'
import { capitalizeFirstLetter, sanitizeString } from '@package/utils'
import { useMemo } from 'react'
import { pidSchemes } from '../constants'

type Attributes = {
given_name: string
Expand Down Expand Up @@ -78,7 +79,7 @@ const attributeNameMapping = {
export function getPidAttributesForDisplay(
attributes: Partial<PidMdocAttributes | PidSdJwtVcAttributes>,
metadata: CredentialMetadata,
claimFormat: ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
claimFormat: ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
) {
if (claimFormat === ClaimFormat.SdJwtVc) {
return getSdJwtPidAttributesForDisplay(attributes, metadata)
Expand Down Expand Up @@ -242,7 +243,7 @@ export function getMdocPidAttributesForDisplay(attributes: Partial<PidMdocAttrib

export function getPidDisclosedAttributeNames(
attributes: Partial<PidMdocAttributes | PidSdJwtVcAttributes>,
claimFormat: ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
claimFormat: ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
) {
if (claimFormat === ClaimFormat.SdJwtVc) {
return getSdJwtPidDisclosedAttributeNames(attributes)
Expand Down Expand Up @@ -406,9 +407,7 @@ export function usePidCredential() {
const { isLoading: isSeedCredentialLoading } = useSeedCredentialPidData()

const pidCredential = useMemo(() => {
const credential = credentials.find(
(cred) => cred.metadata.type === 'https://example.bmi.bund.de/credential/pid/1.0'
)
const credential = credentials.find((cred) => pidSchemes.sdJwtVcVcts.includes(cred.metadata.type))
if (credential) {
const attributes = credential.attributes as PidSdJwtVcAttributes
return {
Expand Down
10 changes: 5 additions & 5 deletions apps/easypid/src/use-cases/ReceivePidUseCaseCFlow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// import type { MdocRecord } from '@credo-ts/core'
import type { MdocRecord } from '@credo-ts/core'
import { pidSchemes } from '@easypid/constants'
import {
BiometricAuthenticationError,
Expand All @@ -8,15 +8,15 @@ import {
storeCredential,
} from '@package/agent'
import { ReceivePidUseCaseFlow, type ReceivePidUseCaseFlowOptions } from './ReceivePidUseCaseFlow'
import { C_SD_JWT_OFFER } from './bdrPidIssuerOffers'
import { C_SD_JWT_MDOC_OFFER } from './bdrPidIssuerOffers'

export class ReceivePidUseCaseCFlow extends ReceivePidUseCaseFlow {
private static REDIRECT_URI = 'https://funke.animo.id/redirect'

public static async initialize(options: ReceivePidUseCaseFlowOptions) {
const resolved = await resolveOpenId4VciOffer({
agent: options.agent,
offer: { uri: C_SD_JWT_OFFER },
offer: { uri: C_SD_JWT_MDOC_OFFER },
authorization: {
clientId: ReceivePidUseCaseCFlow.CLIENT_ID,
redirectUri: ReceivePidUseCaseCFlow.REDIRECT_URI,
Expand Down Expand Up @@ -58,14 +58,14 @@ export class ReceivePidUseCaseCFlow extends ReceivePidUseCaseFlow {

for (const credentialRecord of credentialRecords) {
if (typeof credentialRecord === 'string') throw new Error('No string expected for c flow')
if (credentialRecord.type !== 'SdJwtVcRecord' /*&& credentialRecord.type !== 'MdocRecord' */) {
if (credentialRecord.type !== 'SdJwtVcRecord' && credentialRecord.type !== 'MdocRecord') {
throw new Error(`Unexpected record type ${credentialRecord.type}`)
}

await storeCredential(this.options.agent, credentialRecord)
}

return credentialRecords as Array<SdJwtVcRecord /*| MdocRecord */>
return credentialRecords as Array<SdJwtVcRecord | MdocRecord>
} catch (error) {
// We can recover from this error, so we shouldn't set the state to error
if (error instanceof BiometricAuthenticationError) {
Expand Down
5 changes: 2 additions & 3 deletions apps/easypid/src/use-cases/ReceivePidUseCaseFlow.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { AusweisAuthFlow, type AusweisAuthFlowOptions, sendCommand } from '@animo-id/expo-ausweis-sdk'
// import type { MdocRecord } from '@credo-ts/core'
import type { MdocRecord } from '@credo-ts/core'
import type { AppAgent } from '@easypid/agent'
import {
type OpenId4VcCredentialMetadata,
type OpenId4VciRequestTokenResponse,
type OpenId4VciResolvedAuthorizationRequest,
type OpenId4VciResolvedCredentialOffer,
Expand Down Expand Up @@ -57,7 +56,7 @@ export abstract class ReceivePidUseCaseFlow<ExtraOptions = {}> {
},
]

public abstract retrieveCredentials(): Promise<Array<SdJwtVcRecord /* | MdocRecord */>>
public abstract retrieveCredentials(): Promise<Array<SdJwtVcRecord | MdocRecord>>

protected constructor(
options: ReceivePidUseCaseFlowOptions & ExtraOptions,
Expand Down
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,10 @@
"@sphereon/oid4vci-common": "https://gitpkg.vercel.app/animo/OID4VC/packages/oid4vci-common?funke",
"@sphereon/oid4vci-issuer": "https://gitpkg.vercel.app/animo/OID4VC/packages/issuer?funke",
"@sphereon/oid4vci-client": "https://gitpkg.vercel.app/animo/OID4VC/packages/client?funke",
"@sphereon/jarm": "https://gitpkg.vercel.app/animo/OID4VC/packages/jarm?funke",
"@sphereon/ssi-types": "0.29.1-unstable.208"
"@sphereon/jarm": "https://gitpkg.vercel.app/animo/OID4VC/packages/jarm?funke"
},
"patchedDependencies": {
"@hyperledger/[email protected]": "patches/@[email protected]",
"@credo-ts/[email protected]": "patches/@[email protected]"
"@hyperledger/[email protected]": "patches/@[email protected]"
}
}
}
1 change: 0 additions & 1 deletion packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
V2ProofProtocol,
WebDidResolver,
WsOutboundTransport,
X509Module,
} from '@credo-ts/core'
import {
IndyVdrAnonCredsRegistry,
Expand Down
57 changes: 29 additions & 28 deletions packages/agent/src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { CredentialForDisplayId } from './hooks'
import type { OpenId4VcCredentialMetadata } from './openid4vc/metadata'
import type { W3cCredentialJson, W3cIssuerJson } from './types'

import { ClaimFormat, Hasher, JsonTransformer, /* Mdoc, MdocRecord, */ SdJwtVcRecord } from '@credo-ts/core'
import { ClaimFormat, Hasher, JsonTransformer, Mdoc, MdocRecord, SdJwtVcRecord } from '@credo-ts/core'
import { formatDate, getHostNameFromUrl, sanitizeString } from '@package/utils'
import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode'

Expand Down Expand Up @@ -366,7 +366,7 @@ export function getDisclosedAttributePaths(payload: object, prefix = ''): Array<
return attributes
}

export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord | SdJwtVcRecord /* | MdocRecord */) {
export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord | SdJwtVcRecord | MdocRecord) {
if (credentialRecord instanceof SdJwtVcRecord) {
// FIXME: we should probably add a decode method on the SdJwtVcRecord
// as you now need the agent context to decode the sd-jwt vc, while that's
Expand Down Expand Up @@ -394,32 +394,33 @@ export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord |
claimFormat: ClaimFormat.SdJwtVc,
}
}
// if (credentialRecord instanceof MdocRecord) {
// const openId4VcMetadata = getOpenId4VcCredentialMetadata(credentialRecord)
// const issuerDisplay = getOpenId4VcIssuerDisplay(openId4VcMetadata)
// const credentialDisplay = getMdocCredentialDisplay({}, openId4VcMetadata)

// const mdocInstance = Mdoc.fromIssuerSignedHex(credentialRecord.issuerSignedHex)

// const attributes = Object.fromEntries(Object.values(mdocInstance.namespaces).flatMap((a) => Object.entries(a)))

// return {
// id: `mdoc-${credentialRecord.id}` satisfies CredentialForDisplayId,
// createdAt: credentialRecord.createdAt,
// display: {
// ...credentialDisplay,
// issuer: issuerDisplay,
// },
// attributes,
// // TODO:
// metadata: {
// holder: 'Unknown',
// issuer: 'Unknown',
// type: mdocInstance.docType,
// } satisfies CredentialMetadata,
// claimFormat: ClaimFormat.MsoMdoc,
// }
// }
if (credentialRecord instanceof MdocRecord) {
const openId4VcMetadata = getOpenId4VcCredentialMetadata(credentialRecord)
const issuerDisplay = getOpenId4VcIssuerDisplay(openId4VcMetadata)
const credentialDisplay = getMdocCredentialDisplay({}, openId4VcMetadata)

const mdocInstance = Mdoc.fromBase64Url(credentialRecord.base64Url)
const attributes = Object.fromEntries(
Object.values(mdocInstance.issuerSignedNamespaces).flatMap((a) => Object.entries(a))
)

return {
id: `mdoc-${credentialRecord.id}` satisfies CredentialForDisplayId,
createdAt: credentialRecord.createdAt,
display: {
...credentialDisplay,
issuer: issuerDisplay,
},
attributes,
// TODO:
metadata: {
holder: 'Unknown',
issuer: 'Unknown',
type: mdocInstance.docType,
} satisfies CredentialMetadata,
claimFormat: ClaimFormat.MsoMdoc,
}
}

const credential = JsonTransformer.toJSON(
credentialRecord.credential.claimFormat === ClaimFormat.JwtVc
Expand Down
4 changes: 2 additions & 2 deletions packages/agent/src/format/formatPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ export function formatDifPexCredentialsForRequest(
let disclosedPayload = attributes
if (verifiableCredential.type === ClaimFormat.SdJwtVc) {
disclosedPayload = filterAndMapSdJwtKeys(verifiableCredential.disclosedPayload).visibleProperties
} /* else if (verifiableCredential.type === ClaimFormat.MsoMdoc) {
} else if (verifiableCredential.type === ClaimFormat.MsoMdoc) {
disclosedPayload = Object.fromEntries(
Object.values(verifiableCredential.disclosedPayload).flatMap((entry) => Object.entries(entry))
)
} */
}

return {
id: verifiableCredential.credentialRecord.id,
Expand Down
8 changes: 7 additions & 1 deletion packages/agent/src/hooks/useCredentialForDisplayById.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getCredentialForDisplay } from '../display'
import { useSdJwtVcRecordById, useW3cCredentialRecordById } from '../providers'
import { useMdocRecordById, useSdJwtVcRecordById, useW3cCredentialRecordById } from '../providers'

export type CredentialForDisplayId = `w3c-credential-${string}` | `sd-jwt-vc-${string}` | `mdoc-${string}`

Expand All @@ -16,6 +16,12 @@ export const useCredentialForDisplayById = (credentialId: CredentialForDisplayId

return getCredentialForDisplay(c)
}
if (credentialId.startsWith('mdoc-')) {
const c = useMdocRecordById(credentialId.replace('mdoc-', ''))
if (!c) return null

return getCredentialForDisplay(c)
}

// TODO: add mdoc
return null
Expand Down
Loading

0 comments on commit 52497eb

Please sign in to comment.