import qrcode component dynamically

This commit is contained in:
turnoffthiscomputer
2024-10-14 16:17:20 -07:00
parent 8d4441e433
commit 3a96f37d03
8 changed files with 301 additions and 296 deletions

View File

@@ -1,9 +1,8 @@
{
"name": "@openpassport/sdk",
"version": "0.1.8",
"main": "dist/bundle.node.js",
"browser": "dist/bundle.web.js",
"types": "dist/sdk/src/index.d.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
@@ -57,15 +56,10 @@
"ts-loader": "^9.5.1",
"ts-mocha": "^10.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4"
"typescript": "^5.4.5"
},
"scripts": {
"build:ts": "tsc && tsc -p tsconfig.react.json",
"build:web": "webpack --config webpack.web.config.js",
"build:node": "webpack --config webpack.node.config.js",
"build": "npm run build:ts && npm run build:web && npm run build:node",
"build": "tsc",
"prepublishOnly": "npm run build",
"test": "yarn ts-mocha -p ./tsconfig.json tests/openPassportVerifier.test.ts --exit",
"install-sdk": "cd ../common && yarn && cd ../sdk && yarn",
@@ -87,4 +81,4 @@
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
}

View File

@@ -1,276 +1,279 @@
import { groth16 } from 'snarkjs';
import {
n_dsc,
k_dsc,
ECDSA_K_LENGTH_FACTOR,
k_dsc_ecdsa,
circuitNameFromMode,
countryNames,
countryCodes,
n_dsc,
k_dsc,
ECDSA_K_LENGTH_FACTOR,
k_dsc_ecdsa,
circuitNameFromMode,
countryNames,
countryCodes,
} from '../../common/src/constants/constants';
import {
areArraysEqual,
getCurrentDateFormatted,
getVkeyFromArtifacts,
verifyDSCValidity,
areArraysEqual,
getCurrentDateFormatted,
getVkeyFromArtifacts,
verifyDSCValidity,
} from '../utils/utils';
import { OpenPassportAttestation, OpenPassportVerifierReport } from './index.web';
import {
parsePublicSignalsDsc,
parsePublicSignalsProve,
OpenPassportAttestation
} from '../../common/src/utils/openPassportAttestation';
import {
parsePublicSignalsDsc,
parsePublicSignalsProve,
} from '../../common/src/utils/openPassportAttestation';
import { Mode } from 'fs';
import forge from 'node-forge';
import { parseCertificate } from '../../common/src/utils/certificates/handleCertificate';
import {
castToScope,
formatForbiddenCountriesListFromCircuitOutput,
getAttributeFromUnpackedReveal,
getOlderThanFromCircuitOutput,
splitToWords,
castToScope,
formatForbiddenCountriesListFromCircuitOutput,
getAttributeFromUnpackedReveal,
getOlderThanFromCircuitOutput,
splitToWords,
} from '../../common/src/utils/utils';
import { unpackReveal } from '../../common/src/utils/revealBitmap';
import { getCSCAModulusMerkleTree } from '../../common/src/utils/csca';
import { OpenPassportVerifierReport } from './OpenPassportVerifierReport';
export class AttestationVerifier {
protected devMode: boolean;
protected scope: string;
protected report: OpenPassportVerifierReport;
protected minimumAge: { enabled: boolean; value: string } = { enabled: false, value: '18' };
protected nationality: { enabled: boolean; value: (typeof countryNames)[number] } = {
enabled: false,
value: '' as (typeof countryNames)[number],
};
protected excludedCountries: { enabled: boolean; value: (typeof countryNames)[number][] } = {
enabled: false,
value: [],
};
protected ofac: boolean = false;
protected devMode: boolean;
protected scope: string;
protected report: OpenPassportVerifierReport;
protected minimumAge: { enabled: boolean; value: string } = { enabled: false, value: '18' };
protected nationality: { enabled: boolean; value: (typeof countryNames)[number] } = {
enabled: false,
value: '' as (typeof countryNames)[number],
};
protected excludedCountries: { enabled: boolean; value: (typeof countryNames)[number][] } = {
enabled: false,
value: [],
};
protected ofac: boolean = false;
constructor(devMode: boolean = false) {
this.devMode = devMode;
this.report = new OpenPassportVerifierReport();
}
public async verify(attestation: OpenPassportAttestation): Promise<OpenPassportVerifierReport> {
const kScaled =
attestation.proof.signatureAlgorithm === 'ecdsa'
? ECDSA_K_LENGTH_FACTOR * k_dsc_ecdsa
: k_dsc;
const parsedPublicSignals = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
kScaled
);
this.verifyAttribute('scope', castToScope(parsedPublicSignals.scope), this.scope);
await this.verifyProof(
attestation.proof.mode,
attestation.proof.value.proof,
attestation.proof.value.publicSignals,
attestation.proof.signatureAlgorithm,
attestation.proof.hashFunction
);
switch (attestation.proof.mode) {
case 'register':
await this.verifyRegister(attestation);
break;
case 'prove_onchain':
await this.verifyProveOnChain(attestation);
break;
case 'prove_offchain':
await this.verifyProveOffChain(attestation);
break;
case 'disclose':
await this.verifyDisclose(attestation);
break;
constructor(devMode: boolean = false) {
this.devMode = devMode;
this.report = new OpenPassportVerifierReport();
}
return this.report;
}
private async verifyRegister(attestation: OpenPassportAttestation) {
// verify dscProof
await this.verifyDscProof(attestation);
// verify that the blinded dscCommitments of proof and dscProof match
this.verifyBlindedDscCommitments(attestation);
// verify the root of the csca merkle tree
this.verifyCSCARoot(attestation);
}
private async verifyProveOffChain(attestation: OpenPassportAttestation) {
// verify disclose attributes
this.verifyDiscloseAttributes(attestation);
// verify certificate chain
this.verifyCertificateChain(attestation);
}
private async verifyProveOnChain(attestation: OpenPassportAttestation) {
// verify attributes
this.verifyDiscloseAttributes(attestation);
// verify the dsc proof
await this.verifyDscProof(attestation);
// verify that the blinded dscCommitments of proof and dscProof match
this.verifyBlindedDscCommitments(attestation);
// verify the root of the csca merkle tree
this.verifyCSCARoot(attestation);
}
private async verifyDisclose(attestation: OpenPassportAttestation) {
// verify the root of the commitment
this.verifyCommitment(attestation);
// verify disclose attributes
this.verifyDiscloseAttributes(attestation);
}
private verifyDiscloseAttributes(attestation: OpenPassportAttestation) {
const parsedPublicSignals = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
k_dsc
);
this.verifyAttribute(
'current_date',
parsedPublicSignals.current_date.toString(),
getCurrentDateFormatted().toString()
);
const unpackedReveal = unpackReveal(parsedPublicSignals.revealedData_packed);
if (this.minimumAge.enabled) {
const attributeValueInt = getOlderThanFromCircuitOutput(parsedPublicSignals.older_than);
const selfAttributeOlderThan = parseInt(this.minimumAge.value);
if (attributeValueInt < selfAttributeOlderThan) {
this.report.exposeAttribute(
'older_than',
attributeValueInt.toString(),
this.minimumAge.value.toString()
public async verify(attestation: OpenPassportAttestation): Promise<OpenPassportVerifierReport> {
const kScaled =
attestation.proof.signatureAlgorithm === 'ecdsa'
? ECDSA_K_LENGTH_FACTOR * k_dsc_ecdsa
: k_dsc;
const parsedPublicSignals = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
kScaled
);
} else {
console.log('\x1b[32m%s\x1b[0m', '- minimum age verified');
}
}
if (this.nationality.enabled) {
const attributeValue = getAttributeFromUnpackedReveal(unpackedReveal, 'nationality');
this.verifyAttribute('nationality', countryCodes[attributeValue], this.nationality.value);
}
if (this.ofac) {
const attributeValue = parsedPublicSignals.ofac_result.toString();
this.verifyAttribute('ofac', attributeValue, '1');
}
if (this.excludedCountries.enabled) {
const formattedCountryList = formatForbiddenCountriesListFromCircuitOutput(
parsedPublicSignals.forbidden_countries_list_packed_disclosed
);
const formattedCountryListFullCountryNames = formattedCountryList.map(
(countryCode) => countryCodes[countryCode]
);
this.verifyAttribute(
'forbidden_countries_list',
formattedCountryListFullCountryNames.toString(),
this.excludedCountries.value.toString()
);
}
}
private verifyCSCARoot(attestation: OpenPassportAttestation) {
// verify the root of the csca merkle tree
const parsedDscPublicSignals = parsePublicSignalsDsc(attestation.dscProof.value.publicSignals);
const cscaMerkleTreeFromDscProof = parsedDscPublicSignals.merkle_root;
const cscaMerkleTree = getCSCAModulusMerkleTree();
const cscaRoot = cscaMerkleTree.root;
this.verifyAttribute('merkle_root_csca', cscaRoot, cscaMerkleTreeFromDscProof);
}
private verifyCommitment(attestation: OpenPassportAttestation) {
// check that the commitment is in the commitment merkle tree
// on chain -> call rpc url et contracts
// off chain -> call enpoints
}
private verifyCertificateChain(attestation: OpenPassportAttestation) {
const dscCertificate = forge.pki.certificateFromPem(attestation.dsc.value);
const verified_certificate = verifyDSCValidity(dscCertificate, this.devMode);
if (!verified_certificate) {
this.report.exposeAttribute('dsc', attestation.dsc.value, 'certificate chain is not valid');
} else {
console.log('\x1b[32m%s\x1b[0m', '- certificate verified');
}
const parsedDsc = parseCertificate(attestation.dsc.value);
const signatureAlgorithmDsc = parsedDsc.signatureAlgorithm;
if (signatureAlgorithmDsc === 'ecdsa') {
throw new Error('ECDSA not supported yet');
} else {
const dscModulus = parsedDsc.modulus;
const dscModulusBigInt = BigInt(`0x${dscModulus}`);
const dscModulusWords = splitToWords(dscModulusBigInt, n_dsc, k_dsc);
const pubKeyFromProof = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
k_dsc
).pubKey_disclosed;
const verified_modulus = areArraysEqual(dscModulusWords, pubKeyFromProof);
if (!verified_modulus) {
this.report.exposeAttribute(
'pubKey',
pubKeyFromProof,
'pubKey from proof does not match pubKey from DSC certificate'
this.verifyAttribute('scope', castToScope(parsedPublicSignals.scope), this.scope);
await this.verifyProof(
attestation.proof.mode,
attestation.proof.value.proof,
attestation.proof.value.publicSignals,
attestation.proof.signatureAlgorithm,
attestation.proof.hashFunction
);
} else {
console.log('\x1b[32m%s\x1b[0m', '- modulus verified');
}
switch (attestation.proof.mode) {
case 'register':
await this.verifyRegister(attestation);
break;
case 'prove_onchain':
await this.verifyProveOnChain(attestation);
break;
case 'prove_offchain':
await this.verifyProveOffChain(attestation);
break;
case 'disclose':
await this.verifyDisclose(attestation);
break;
}
return this.report;
}
}
private verifyBlindedDscCommitments(attestation: OpenPassportAttestation) {
const parsedPublicSignals = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
k_dsc
);
const proofBlindedDscCommitment = parsedPublicSignals.blinded_dsc_commitment;
const parsedDscPublicSignals = parsePublicSignalsDsc(attestation.dscProof.value.publicSignals);
const dscBlindedDscCommitment = parsedDscPublicSignals.blinded_dsc_commitment;
this.verifyAttribute(
'blinded_dsc_commitment',
proofBlindedDscCommitment,
dscBlindedDscCommitment
);
}
private async verifyProof(
mode: Mode,
proof: string[],
publicSignals: string[],
signatureAlgorithm: string,
hashFunction: string
): Promise<void> {
const circuitName = circuitNameFromMode[mode];
const vkey = getVkeyFromArtifacts(circuitName, signatureAlgorithm, hashFunction);
const isVerified = await groth16.verify(vkey, publicSignals, proof as any);
this.verifyAttribute('proof', isVerified.toString(), 'true');
}
private async verifyDscProof(attestation: OpenPassportAttestation) {
const dscSignatureAlgorithm = attestation.dscProof.signatureAlgorithm;
const dscHashFunction = attestation.dscProof.hashFunction;
const vkey = getVkeyFromArtifacts('dsc', dscSignatureAlgorithm, dscHashFunction);
const isVerified = await groth16.verify(
vkey,
attestation.dscProof.value.publicSignals,
attestation.dscProof.value.proof as any
);
this.verifyAttribute('dscProof', isVerified.toString(), 'true');
}
private verifyAttribute(
attribute: keyof OpenPassportVerifierReport,
value: string,
expectedValue: string
) {
if (value !== expectedValue) {
this.report.exposeAttribute(attribute, value, expectedValue);
} else {
console.log('\x1b[32m%s\x1b[0m', `- attribute ${attribute} verified`);
private async verifyRegister(attestation: OpenPassportAttestation) {
// verify dscProof
await this.verifyDscProof(attestation);
// verify that the blinded dscCommitments of proof and dscProof match
this.verifyBlindedDscCommitments(attestation);
// verify the root of the csca merkle tree
this.verifyCSCARoot(attestation);
}
private async verifyProveOffChain(attestation: OpenPassportAttestation) {
// verify disclose attributes
this.verifyDiscloseAttributes(attestation);
// verify certificate chain
this.verifyCertificateChain(attestation);
}
private async verifyProveOnChain(attestation: OpenPassportAttestation) {
// verify attributes
this.verifyDiscloseAttributes(attestation);
// verify the dsc proof
await this.verifyDscProof(attestation);
// verify that the blinded dscCommitments of proof and dscProof match
this.verifyBlindedDscCommitments(attestation);
// verify the root of the csca merkle tree
this.verifyCSCARoot(attestation);
}
private async verifyDisclose(attestation: OpenPassportAttestation) {
// verify the root of the commitment
this.verifyCommitment(attestation);
// verify disclose attributes
this.verifyDiscloseAttributes(attestation);
}
private verifyDiscloseAttributes(attestation: OpenPassportAttestation) {
const parsedPublicSignals = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
k_dsc
);
this.verifyAttribute(
'current_date',
parsedPublicSignals.current_date.toString(),
getCurrentDateFormatted().toString()
);
const unpackedReveal = unpackReveal(parsedPublicSignals.revealedData_packed);
if (this.minimumAge.enabled) {
const attributeValueInt = getOlderThanFromCircuitOutput(parsedPublicSignals.older_than);
const selfAttributeOlderThan = parseInt(this.minimumAge.value);
if (attributeValueInt < selfAttributeOlderThan) {
this.report.exposeAttribute(
'older_than',
attributeValueInt.toString(),
this.minimumAge.value.toString()
);
} else {
console.log('\x1b[32m%s\x1b[0m', '- minimum age verified');
}
}
if (this.nationality.enabled) {
const attributeValue = getAttributeFromUnpackedReveal(unpackedReveal, 'nationality');
this.verifyAttribute('nationality', countryCodes[attributeValue], this.nationality.value);
}
if (this.ofac) {
const attributeValue = parsedPublicSignals.ofac_result.toString();
this.verifyAttribute('ofac', attributeValue, '1');
}
if (this.excludedCountries.enabled) {
const formattedCountryList = formatForbiddenCountriesListFromCircuitOutput(
parsedPublicSignals.forbidden_countries_list_packed_disclosed
);
const formattedCountryListFullCountryNames = formattedCountryList.map(
(countryCode) => countryCodes[countryCode]
);
this.verifyAttribute(
'forbidden_countries_list',
formattedCountryListFullCountryNames.toString(),
this.excludedCountries.value.toString()
);
}
}
private verifyCSCARoot(attestation: OpenPassportAttestation) {
// verify the root of the csca merkle tree
const parsedDscPublicSignals = parsePublicSignalsDsc(attestation.dscProof.value.publicSignals);
const cscaMerkleTreeFromDscProof = parsedDscPublicSignals.merkle_root;
const cscaMerkleTree = getCSCAModulusMerkleTree();
const cscaRoot = cscaMerkleTree.root;
this.verifyAttribute('merkle_root_csca', cscaRoot, cscaMerkleTreeFromDscProof);
}
private verifyCommitment(attestation: OpenPassportAttestation) {
// check that the commitment is in the commitment merkle tree
// on chain -> call rpc url et contracts
// off chain -> call enpoints
}
private verifyCertificateChain(attestation: OpenPassportAttestation) {
const dscCertificate = forge.pki.certificateFromPem(attestation.dsc.value);
const verified_certificate = verifyDSCValidity(dscCertificate, this.devMode);
if (!verified_certificate) {
this.report.exposeAttribute('dsc', attestation.dsc.value, 'certificate chain is not valid');
} else {
console.log('\x1b[32m%s\x1b[0m', '- certificate verified');
}
const parsedDsc = parseCertificate(attestation.dsc.value);
const signatureAlgorithmDsc = parsedDsc.signatureAlgorithm;
if (signatureAlgorithmDsc === 'ecdsa') {
throw new Error('ECDSA not supported yet');
} else {
const dscModulus = parsedDsc.modulus;
const dscModulusBigInt = BigInt(`0x${dscModulus}`);
const dscModulusWords = splitToWords(dscModulusBigInt, n_dsc, k_dsc);
const pubKeyFromProof = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
k_dsc
).pubKey_disclosed;
const verified_modulus = areArraysEqual(dscModulusWords, pubKeyFromProof);
if (!verified_modulus) {
this.report.exposeAttribute(
'pubKey',
pubKeyFromProof,
'pubKey from proof does not match pubKey from DSC certificate'
);
} else {
console.log('\x1b[32m%s\x1b[0m', '- modulus verified');
}
}
}
private verifyBlindedDscCommitments(attestation: OpenPassportAttestation) {
const parsedPublicSignals = parsePublicSignalsProve(
attestation.proof.value.publicSignals,
k_dsc
);
const proofBlindedDscCommitment = parsedPublicSignals.blinded_dsc_commitment;
const parsedDscPublicSignals = parsePublicSignalsDsc(attestation.dscProof.value.publicSignals);
const dscBlindedDscCommitment = parsedDscPublicSignals.blinded_dsc_commitment;
this.verifyAttribute(
'blinded_dsc_commitment',
proofBlindedDscCommitment,
dscBlindedDscCommitment
);
}
private async verifyProof(
mode: Mode,
proof: string[],
publicSignals: string[],
signatureAlgorithm: string,
hashFunction: string
): Promise<void> {
const circuitName = circuitNameFromMode[mode];
const vkey = getVkeyFromArtifacts(circuitName, signatureAlgorithm, hashFunction);
const isVerified = await groth16.verify(vkey, publicSignals, proof as any);
this.verifyAttribute('proof', isVerified.toString(), 'true');
}
private async verifyDscProof(attestation: OpenPassportAttestation) {
const dscSignatureAlgorithm = attestation.dscProof.signatureAlgorithm;
const dscHashFunction = attestation.dscProof.hashFunction;
const vkey = getVkeyFromArtifacts('dsc', dscSignatureAlgorithm, dscHashFunction);
const isVerified = await groth16.verify(
vkey,
attestation.dscProof.value.publicSignals,
attestation.dscProof.value.proof as any
);
this.verifyAttribute('dscProof', isVerified.toString(), 'true');
}
private verifyAttribute(
attribute: keyof OpenPassportVerifierReport,
value: string,
expectedValue: string
) {
if (value !== expectedValue) {
this.report.exposeAttribute(attribute, value, expectedValue);
} else {
console.log('\x1b[32m%s\x1b[0m', `- attribute ${attribute} verified`);
}
}
}
}

View File

@@ -1,4 +1,5 @@
import {
ArgumentsDisclose,
ArgumentsProveOffChain,
ArgumentsProveOnChain,
ArgumentsRegister,
@@ -15,7 +16,7 @@ import { OpenPassportApp } from '../../common/src/utils/appType';
import { UserIdType } from '../../common/src/utils/utils';
import * as pako from 'pako';
import msgpack from 'msgpack-lite';
import { OpenPassportAttestation } from './index.web';
import { OpenPassportAttestation } from '.';
import { AttestationVerifier } from './AttestationVerifier';
export class OpenPassportVerifier extends AttestationVerifier {
private mode: Mode;
@@ -124,6 +125,19 @@ export class OpenPassportVerifier extends AttestationVerifier {
};
openPassportArguments = argsRegisterOnChain;
break;
// case 'vc_and_disclose':
// const argsVcAndDisclose: ArgumentsDisclose = {
// disclosureOptions: {
// minimumAge: this.minimumAge,
// nationality: this.nationality,
// excludedCountries: this.excludedCountries,
// ofac: this.ofac,
// },
// merkleRoot: this.merkleRoot,
// merkletreeSize: this.merkletreeSize,
// };
// openPassportArguments = argsVcAndDisclose;
// break;
}
const intent: OpenPassportApp = {

View File

@@ -1,22 +1,18 @@
import React from 'react';
import { OpenPassportAttestation, OpenPassportVerifier } from '..';
import { UserIdType } from '../../../common/src/utils/utils';
export interface OpenPassportQRcodeProps {
interface OpenPassportQRcodeProps {
appName: string;
scope: string;
userId?: string;
userIdType?: UserIdType;
olderThan?: string;
nationality?: string;
onSuccess?: (result: any) => void;
circuit?: string;
size?: number;
devMode?: boolean;
userId: string;
userIdType: UserIdType;
openPassportVerifier: OpenPassportVerifier;
onSuccess: (attestation: OpenPassportAttestation) => void;
websocketUrl?: string;
merkleTreeUrl?: string;
attestationId?: string;
size?: number;
}
declare const OpenPassportQRcode: React.FC<OpenPassportQRcodeProps>;
export default OpenPassportQRcode;
export { OpenPassportQRcode, OpenPassportQRcodeProps };

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { OpenPassportAttestation, OpenPassportVerifier } from '../index.web';
import { OpenPassportAttestation } from '../../../common/src/utils/openPassportAttestation';
import { OpenPassportVerifier } from '../OpenPassportVerifier';
import { BounceLoader } from 'react-spinners';
import Lottie from 'lottie-react';
import CHECK_ANIMATION from './animations/check_animation.json';

View File

@@ -1,16 +0,0 @@
import { OpenPassportVerifierReport } from './OpenPassportVerifierReport';
import { countryCodes } from '../../common/src/constants/constants';
import { AppType } from '../../common/src/utils/appType';
import { OpenPassportVerifier } from './OpenPassportVerifier';
import {
OpenPassportDynamicAttestation,
OpenPassportAttestation,
} from '../../common/src/utils/openPassportAttestation';
export {
AppType,
OpenPassportVerifier,
OpenPassportDynamicAttestation,
OpenPassportAttestation,
OpenPassportVerifierReport,
countryCodes,
};

View File

@@ -1,12 +1,20 @@
import { OpenPassportVerifierReport } from './OpenPassportVerifierReport';
import { countryCodes } from '../../common/src/constants/constants';
import { OpenPassportVerifier } from './OpenPassportVerifier';
import { OpenPassportQRcode } from './QRcode/OpenPassportQRcode';
import {
OpenPassportAttestation,
OpenPassportDynamicAttestation,
} from '../../common/src/utils/openPassportAttestation';
function isBrowser() {
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
}
let OpenPassportQRcode;
if (isBrowser()) {
OpenPassportQRcode = require('./QRcode/OpenPassportQRcode').OpenPassportQRcode;
}
export {
OpenPassportVerifier,
OpenPassportAttestation,

View File

@@ -14,7 +14,7 @@
"moduleResolution": "node"
},
"include": [
"src/index.web.ts",
"src/index.ts",
"src/index.node.ts",
"src/**/*",
"common/**/*",
@@ -23,5 +23,10 @@
"utils/utils.ts",
"../common/src/utils/openPassportAttestation.ts"
],
"exclude": ["node_modules", "**/__tests__/*", "dist", "common/src/utils/csca.ts"]
}
"exclude": [
"node_modules",
"**/__tests__/*",
"dist",
"common/src/utils/csca.ts"
]
}