diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d6d3291..5fd5d4c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3716,12 +3716,21 @@ importers: services/trust-score: dependencies: + axios: + specifier: ^1.13.6 + version: 1.13.6 cors: specifier: ^2.8.5 version: 2.8.6 express: specifier: ^4.21.2 version: 4.22.1 + graphql: + specifier: ^16.13.1 + version: 16.13.1 + graphql-request: + specifier: ^7.4.0 + version: 7.4.0(graphql@16.13.1) devDependencies: nodemon: specifier: ^3.0.2 diff --git a/services/ontology/schemas/reference.json b/services/ontology/schemas/reference.json index 33294475..af4fedab 100644 --- a/services/ontology/schemas/reference.json +++ b/services/ontology/schemas/reference.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "schemaId": "7e94fada-581d-49b0-b6f5-1fe0766da360", + "schemaId": "c20e9437-02a4-4917-8cee-de35dabbdb6a", "title": "Reference", "type": "object", "properties": { diff --git a/services/trust-score/package-lock.json b/services/trust-score/package-lock.json new file mode 100644 index 00000000..33e3113a --- /dev/null +++ b/services/trust-score/package-lock.json @@ -0,0 +1,571 @@ +{ + "name": "trust-score", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trust-score", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.13.6", + "cors": "^2.8.5", + "express": "^4.21.2", + "graphql": "^16.13.1", + "graphql-request": "^7.4.0" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "vitest": "^2.1.0" + } + }, + "../../node_modules/.pnpm/cors@2.8.6/node_modules/cors": { + "version": "2.8.6", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "devDependencies": { + "after": "0.8.2", + "eslint": "7.30.0", + "express": "4.21.2", + "mocha": "9.2.2", + "nyc": "15.1.0", + "supertest": "6.1.3" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../node_modules/.pnpm/express@4.22.1/node_modules/express": { + "version": "4.22.1", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "devDependencies": { + "after": "0.8.2", + "connect-redis": "3.4.2", + "cookie-parser": "1.4.6", + "cookie-session": "2.0.0", + "ejs": "3.1.9", + "eslint": "8.47.0", + "express-session": "1.17.2", + "hbs": "4.2.0", + "marked": "0.7.0", + "method-override": "3.0.0", + "mocha": "^6.2.2", + "morgan": "1.10.0", + "nyc": "^14.1.1", + "pbkdf2-password": "1.2.1", + "supertest": "^6.1.6", + "vhost": "~3.0.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../node_modules/.pnpm/nodemon@3.1.14/node_modules/nodemon": { + "version": "3.1.14", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "devDependencies": { + "@commitlint/cli": "^11.0.0", + "@commitlint/config-conventional": "^11.0.0", + "async": "1.4.2", + "coffee-script": "~1.7.1", + "eslint": "^7.32.0", + "husky": "^7.0.4", + "mocha": "^2.5.3", + "nyc": "^15.1.0", + "proxyquire": "^1.8.0", + "semantic-release": "^25.0.0", + "should": "~4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "../../node_modules/.pnpm/vitest@2.1.9_@types+node@24.12.0_jsdom@19.0.0_bufferutil@4.1.0__lightningcss@1.31.1_sass@1.98.0_terser@5.46.0/node_modules/vitest": { + "version": "2.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "devDependencies": { + "@ampproject/remapping": "^2.3.0", + "@antfu/install-pkg": "^0.4.1", + "@edge-runtime/vm": "^4.0.4", + "@sinonjs/fake-timers": "11.1.0", + "@types/debug": "^4.1.12", + "@types/estree": "^1.0.6", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/jsdom": "^21.1.7", + "@types/micromatch": "^4.0.9", + "@types/node": "^22.9.0", + "@types/prompts": "^2.4.9", + "@types/sinonjs__fake-timers": "^8.1.5", + "acorn-walk": "^8.3.4", + "birpc": "0.2.19", + "cac": "^6.7.14", + "chai-subset": "^1.6.0", + "cli-truncate": "^4.0.0", + "fast-glob": "3.3.2", + "find-up": "^6.3.0", + "flatted": "^3.3.1", + "get-tsconfig": "^4.8.1", + "happy-dom": "^15.11.4", + "jsdom": "^25.0.1", + "local-pkg": "^0.5.0", + "log-update": "^5.0.1", + "micromatch": "^4.0.8", + "pretty-format": "^29.7.0", + "prompts": "^2.4.2", + "strip-literal": "^2.1.0", + "ws": "^8.18.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cors": { + "resolved": "../../node_modules/.pnpm/cors@2.8.6/node_modules/cors", + "link": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/express": { + "resolved": "../../node_modules/.pnpm/express@4.22.1/node_modules/express", + "link": true + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphql": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", + "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.4.0.tgz", + "integrity": "sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "resolved": "../../node_modules/.pnpm/nodemon@3.1.14/node_modules/nodemon", + "link": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/vitest": { + "resolved": "../../node_modules/.pnpm/vitest@2.1.9_@types+node@24.12.0_jsdom@19.0.0_bufferutil@4.1.0__lightningcss@1.31.1_sass@1.98.0_terser@5.46.0/node_modules/vitest", + "link": true + } + } +} diff --git a/services/trust-score/package.json b/services/trust-score/package.json index ff3415ea..9bb05b0e 100644 --- a/services/trust-score/package.json +++ b/services/trust-score/package.json @@ -13,8 +13,11 @@ "author": "", "license": "ISC", "dependencies": { + "axios": "^1.13.6", "cors": "^2.8.5", - "express": "^4.21.2" + "express": "^4.21.2", + "graphql": "^16.13.1", + "graphql-request": "^7.4.0" }, "devDependencies": { "nodemon": "^3.0.2", diff --git a/services/trust-score/src/__tests__/userDataService.test.js b/services/trust-score/src/__tests__/userDataService.test.js index 2d41d241..d9025522 100644 --- a/services/trust-score/src/__tests__/userDataService.test.js +++ b/services/trust-score/src/__tests__/userDataService.test.js @@ -1,71 +1,244 @@ +/** + * Tests for userDataService — mocks axios to avoid real HTTP calls, + * then verifies the trust-data extraction logic against binding documents. + */ + +const mockGet = vi.fn(); +const mockPost = vi.fn(); + +vi.mock("axios", () => { + const axiosMock = { get: mockGet, post: mockPost }; + axiosMock.default = axiosMock; + return { default: axiosMock }; +}); + const { fetchVerificationStatus, fetchAccountAgeDays, fetchKeyLocation, fetchThirdDegreeConnections, fetchUserTrustData, + fetchBindingDocuments, + normalizeEName, + clearPlatformToken, } = require("../userDataService"); // --------------------------------------------------------------------------- -// Individual fetch functions (currently mocked) +// Helpers +// --------------------------------------------------------------------------- + +/** Build a minimal binding-document edge for GraphQL responses. */ +function makeEdge({ id = "doc-1", type, data = {}, signatures = [] }) { + return { + node: { + id, + parsed: { + subject: "@test-user", + type, + data, + signatures, + }, + }, + }; +} + +function makeSig(signer, daysAgo = 0) { + const d = new Date(); + d.setDate(d.getDate() - daysAgo); + return { signer, signature: "0x...", timestamp: d.toISOString() }; +} + +/** Set up axios mocks so registry + GraphQL calls succeed with `edges`. */ +function mockEvault(edges) { + // resolveEVaultUrl + mockGet.mockResolvedValue({ data: { evaultUrl: "http://evault:4000" } }); + // getPlatformToken + GraphQL query + mockPost.mockImplementation((url) => { + if (url.includes("/platforms/certification")) { + return Promise.resolve({ data: { token: "mock-token" } }); + } + // GraphQL query + return Promise.resolve({ + data: { + data: { + bindingDocuments: { edges }, + }, + }, + }); + }); +} + +// --------------------------------------------------------------------------- +// Setup / teardown +// --------------------------------------------------------------------------- + +beforeEach(() => { + clearPlatformToken(); + vi.clearAllMocks(); +}); + +// --------------------------------------------------------------------------- +// normalizeEName +// --------------------------------------------------------------------------- + +describe("normalizeEName", () => { + test("prefixes @ when missing", () => { + expect(normalizeEName("alice")).toBe("@alice"); + }); + test("leaves @ if already present", () => { + expect(normalizeEName("@alice")).toBe("@alice"); + }); +}); + +// --------------------------------------------------------------------------- +// fetchBindingDocuments +// --------------------------------------------------------------------------- + +describe("fetchBindingDocuments", () => { + test("returns parsed documents from GraphQL response", async () => { + const edges = [ + makeEdge({ type: "id_document", data: { vendor: "veriff" } }), + makeEdge({ id: "doc-2", type: "social_connection", signatures: [makeSig("@a"), makeSig("@b")] }), + ]; + mockEvault(edges); + + const docs = await fetchBindingDocuments("test-user"); + expect(docs).toHaveLength(2); + expect(docs[0].type).toBe("id_document"); + expect(docs[1].type).toBe("social_connection"); + }); + + test("filters out malformed documents", async () => { + const edges = [ + { node: { id: "bad", parsed: null } }, + { node: { id: "bad2", parsed: { subject: "@x" } } }, // missing type, data, signatures + makeEdge({ type: "self" }), + ]; + mockEvault(edges); + + const docs = await fetchBindingDocuments("test-user"); + expect(docs).toHaveLength(1); + expect(docs[0].type).toBe("self"); + }); +}); + +// --------------------------------------------------------------------------- +// fetchVerificationStatus // --------------------------------------------------------------------------- + describe("fetchVerificationStatus", () => { - test("returns a boolean", async () => { - const result = await fetchVerificationStatus("test-user"); - expect(typeof result).toBe("boolean"); + test("returns true when id_document exists", async () => { + mockEvault([makeEdge({ type: "id_document" })]); + expect(await fetchVerificationStatus("test-user")).toBe(true); + }); + + test("returns false when no id_document exists", async () => { + mockEvault([makeEdge({ type: "social_connection" })]); + expect(await fetchVerificationStatus("test-user")).toBe(false); }); - test("mock returns false (unverified by default)", async () => { - const result = await fetchVerificationStatus("test-user"); - expect(result).toBe(false); + test("returns false when eVault is empty", async () => { + mockEvault([]); + expect(await fetchVerificationStatus("test-user")).toBe(false); }); }); +// --------------------------------------------------------------------------- +// fetchAccountAgeDays +// --------------------------------------------------------------------------- + describe("fetchAccountAgeDays", () => { - test("returns a number", async () => { - const result = await fetchAccountAgeDays("test-user"); - expect(typeof result).toBe("number"); + test("returns 0 when no documents exist", async () => { + mockEvault([]); + expect(await fetchAccountAgeDays("test-user")).toBe(0); }); - test("returns a non-negative value", async () => { - const result = await fetchAccountAgeDays("test-user"); - expect(result).toBeGreaterThanOrEqual(0); + test("computes days from earliest signature", async () => { + const edges = [ + makeEdge({ + type: "self", + signatures: [makeSig("@test-user", 200)], + }), + makeEdge({ + id: "doc-2", + type: "id_document", + signatures: [makeSig("@verifier", 100)], + }), + ]; + mockEvault(edges); + + const days = await fetchAccountAgeDays("test-user"); + // Earliest is 200 days ago + expect(days).toBeGreaterThanOrEqual(199); + expect(days).toBeLessThanOrEqual(201); }); }); +// --------------------------------------------------------------------------- +// fetchKeyLocation +// --------------------------------------------------------------------------- + describe("fetchKeyLocation", () => { - test('returns "TPM" or "SW"', async () => { - const result = await fetchKeyLocation("test-user"); - expect(["TPM", "SW"]).toContain(result); + test("returns TPM when binding document has keyLocation=TPM", async () => { + mockEvault([makeEdge({ type: "self", data: { keyLocation: "TPM" } })]); + expect(await fetchKeyLocation("test-user")).toBe("TPM"); }); -}); -describe("fetchThirdDegreeConnections", () => { - test("returns a number", async () => { - const result = await fetchThirdDegreeConnections("test-user"); - expect(typeof result).toBe("number"); + test("returns SW when no key location info exists", async () => { + mockEvault([makeEdge({ type: "self", data: {} })]); + expect(await fetchKeyLocation("test-user")).toBe("SW"); }); - test("returns a non-negative value", async () => { - const result = await fetchThirdDegreeConnections("test-user"); - expect(result).toBeGreaterThanOrEqual(0); + test("falls back to data.location field", async () => { + mockEvault([makeEdge({ type: "self", data: { location: "TPM" } })]); + expect(await fetchKeyLocation("test-user")).toBe("TPM"); }); }); // --------------------------------------------------------------------------- -// Aggregated fetch +// fetchThirdDegreeConnections // --------------------------------------------------------------------------- -describe("fetchUserTrustData", () => { - test("returns all required fields", async () => { - const data = await fetchUserTrustData("test-user"); - expect(data).toHaveProperty("isVerified"); - expect(data).toHaveProperty("accountAgeDays"); - expect(data).toHaveProperty("keyLocation"); - expect(data).toHaveProperty("thirdDegreeConnections"); +describe("fetchThirdDegreeConnections", () => { + test("counts social_connection docs with 2 signatures", async () => { + const edges = [ + makeEdge({ + id: "sc1", + type: "social_connection", + signatures: [makeSig("@a"), makeSig("@b")], + }), + makeEdge({ + id: "sc2", + type: "social_connection", + signatures: [makeSig("@a"), makeSig("@c")], + }), + // Only 1 signature — not counted + makeEdge({ + id: "sc3", + type: "social_connection", + signatures: [makeSig("@a")], + }), + // Not a social_connection + makeEdge({ id: "other", type: "id_document" }), + ]; + mockEvault(edges); + + expect(await fetchThirdDegreeConnections("test-user")).toBe(2); + }); + + test("returns 0 when no social connections", async () => { + mockEvault([]); + expect(await fetchThirdDegreeConnections("test-user")).toBe(0); }); +}); + +// --------------------------------------------------------------------------- +// fetchUserTrustData (aggregated, single GraphQL call) +// --------------------------------------------------------------------------- - test("returned data has correct types", async () => { +describe("fetchUserTrustData", () => { + test("returns all required fields with correct types", async () => { + mockEvault([]); const data = await fetchUserTrustData("test-user"); expect(typeof data.isVerified).toBe("boolean"); @@ -74,11 +247,51 @@ describe("fetchUserTrustData", () => { expect(typeof data.thirdDegreeConnections).toBe("number"); }); + test("derives all inputs from a realistic eVault", async () => { + const edges = [ + makeEdge({ + id: "id1", + type: "id_document", + data: { vendor: "veriff" }, + signatures: [makeSig("@test-user", 365)], + }), + makeEdge({ + id: "key1", + type: "self", + data: { keyLocation: "TPM" }, + signatures: [makeSig("@test-user", 365)], + }), + makeEdge({ + id: "sc1", + type: "social_connection", + signatures: [makeSig("@test-user", 200), makeSig("@alice", 200)], + }), + makeEdge({ + id: "sc2", + type: "social_connection", + signatures: [makeSig("@test-user", 100), makeSig("@bob", 100)], + }), + ]; + mockEvault(edges); + + const data = await fetchUserTrustData("test-user"); + expect(data.isVerified).toBe(true); + expect(data.accountAgeDays).toBeGreaterThanOrEqual(364); + expect(data.keyLocation).toBe("TPM"); + expect(data.thirdDegreeConnections).toBe(2); + }); + test("returned data is compatible with calculateTrustScore", async () => { + mockEvault([ + makeEdge({ + id: "id1", + type: "id_document", + signatures: [makeSig("@test-user", 200)], + }), + ]); + const { calculateTrustScore } = require("../score"); const data = await fetchUserTrustData("test-user"); - - // Should not throw — the shape matches what calculateTrustScore expects const result = calculateTrustScore(data); expect(result).toHaveProperty("score"); diff --git a/services/trust-score/src/index.js b/services/trust-score/src/index.js index 95ce5825..221178c9 100644 --- a/services/trust-score/src/index.js +++ b/services/trust-score/src/index.js @@ -1,7 +1,8 @@ +const path = require("path"); const express = require("express"); const cors = require("cors"); const { calculateTrustScore } = require("./score"); -const { fetchUserTrustData } = require("./userDataService"); +const { fetchUserTrustData, setRegistryUrl } = require("./userDataService"); const app = express(); const PORT = process.env.PORT || 3005; @@ -10,11 +11,24 @@ const PORT = process.env.PORT || 3005; app.use(cors()); app.use(express.json()); +// Serve the test UI +app.use(express.static(path.join(__dirname, "public"))); + // Health check app.get("/health", (_req, res) => { res.json({ status: "ok" }); }); +// Runtime config — set registry URL from UI +app.post("/config", (req, res) => { + const { registryUrl } = req.body; + if (!registryUrl || typeof registryUrl !== "string") { + return res.status(400).json({ error: "registryUrl is required" }); + } + setRegistryUrl(registryUrl); + return res.json({ ok: true, registryUrl }); +}); + /** * GET /trust-score/:eName * diff --git a/services/trust-score/src/public/index.html b/services/trust-score/src/public/index.html new file mode 100644 index 00000000..f6e8ab67 --- /dev/null +++ b/services/trust-score/src/public/index.html @@ -0,0 +1,169 @@ + + + + + +Trust Score Tester + + + +
+

Trust Score Tester

+ + + + +
+ +
+ + + + + +
+
0 / 10
+
+
+
KYC Verified
+
-
+
+
+
Account Age
+
-
+
+
+
Key Location
+
-
+
+
+
Social Connections
+
-
+
+
+
+ Raw JSON +

+    
+
+ + +
+ + + + diff --git a/services/trust-score/src/userDataService.js b/services/trust-score/src/userDataService.js index 7bdceb27..c3f1d58a 100644 --- a/services/trust-score/src/userDataService.js +++ b/services/trust-score/src/userDataService.js @@ -1,109 +1,244 @@ /** * User Data Service * - * Responsible for fetching all the data inputs the trust score needs - * for a given user (identified by their eName / W3ID). + * Fetches all trust-score inputs for a given user (by eName / W3ID) + * from their eVault binding documents. * - * Currently MOCKED — each function returns placeholder data. - * Once binding documents are written to the eVault, replace each mock - * with a real API call. + * Flow (mirrors platforms/enotary): + * 1. Resolve eName → eVault URL via the registry + * 2. Obtain a platform bearer token from the registry + * 3. Query the eVault GraphQL API for binding documents + * 4. Derive verification status, account age, key location, + * and social-connection count from the returned documents */ -const EVAULT_CORE_URL = process.env.EVAULT_CORE_URL || "http://localhost:3001"; -const EVAULT_DATA_URL = process.env.EVAULT_DATA_URL || "http://localhost:4000"; +const axios = require("axios"); + +let REGISTRY_URL = + process.env.REGISTRY_URL || + process.env.PUBLIC_REGISTRY_URL || + "http://localhost:4321"; + +function setRegistryUrl(url) { + REGISTRY_URL = url; + // Clear cached token since it belongs to the old registry + _platformToken = null; +} + +const BINDING_DOCUMENTS_QUERY = ` + query GetBindingDocuments($first: Int!) { + bindingDocuments(first: $first) { + edges { + node { + id + parsed + } + } + } + } +`; + +// --------------------------------------------------------------------------- +// Registry helpers +// --------------------------------------------------------------------------- + +let _platformToken = null; + +function normalizeEName(value) { + return value.startsWith("@") ? value : `@${value}`; +} /** - * Fetch the user's KYC verification status from evault-core. - * - * Real implementation will query: - * GET {EVAULT_CORE_URL}/verification?linkedEName={eName} - * → look at the `approved` field on the Verification entity + * Resolve an eName to its eVault base URL via the registry. + */ +async function resolveEVaultUrl(eName) { + const normalized = normalizeEName(eName); + const endpoint = new URL( + `/resolve?w3id=${encodeURIComponent(normalized)}`, + REGISTRY_URL, + ).toString(); + + const response = await axios.get(endpoint, { timeout: 10_000 }); + const resolved = response.data?.evaultUrl || response.data?.uri; + if (!resolved) { + throw new Error(`Registry did not return an eVault URL for ${normalized}`); + } + return resolved; +} + +/** + * Obtain a platform bearer token from the registry. + */ +async function getPlatformToken() { + if (_platformToken) return _platformToken; + + const endpoint = new URL("/platforms/certification", REGISTRY_URL).toString(); + const response = await axios.post( + endpoint, + { platform: "trust-score" }, + { timeout: 10_000 }, + ); + _platformToken = response.data.token; + return _platformToken; +} + +/** Reset cached token (useful when a token expires). */ +function clearPlatformToken() { + _platformToken = null; +} + +// --------------------------------------------------------------------------- +// eVault GraphQL helper +// --------------------------------------------------------------------------- + +/** + * Fetch all binding documents from a user's eVault. * - * @param {string} eName The user's W3ID / eName - * @returns {Promise} + * @param {string} eName + * @returns {Promise>} + */ +async function fetchBindingDocuments(eName) { + const normalized = normalizeEName(eName); + const [evaultBaseUrl, token] = await Promise.all([ + resolveEVaultUrl(normalized), + getPlatformToken(), + ]); + + const graphqlUrl = new URL("/graphql", evaultBaseUrl).toString(); + + const response = await axios.post( + graphqlUrl, + { query: BINDING_DOCUMENTS_QUERY, variables: { first: 100 } }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-ENAME": normalized, + }, + timeout: 10_000, + }, + ); + + const edges = response.data?.data?.bindingDocuments?.edges ?? []; + + return edges + .map((edge) => { + const parsed = edge.node?.parsed; + if (!parsed || typeof parsed !== "object") return null; + const { subject, type, data, signatures } = parsed; + if ( + typeof subject !== "string" || + typeof type !== "string" || + typeof data !== "object" || + data === null || + !Array.isArray(signatures) + ) { + return null; + } + return { id: edge.node.id, subject, type, data, signatures }; + }) + .filter(Boolean); +} + +// --------------------------------------------------------------------------- +// Individual trust-data extractors +// --------------------------------------------------------------------------- + +/** + * Is the user KYC-verified? + * True when at least one `id_document` binding document exists. */ async function fetchVerificationStatus(eName) { - // TODO: Replace with real evault-core API call - // const res = await fetch(`${EVAULT_CORE_URL}/verification?linkedEName=${eName}`); - // const data = await res.json(); - // return data.approved === true; - return false; + const docs = await fetchBindingDocuments(eName); + return docs.some((doc) => doc.type === "id_document"); } /** - * Fetch the user's account age in days from evault-core. - * - * Real implementation will query: - * GET {EVAULT_CORE_URL}/verification?linkedEName={eName} - * → compute days since `createdAt` - * - * @param {string} eName The user's W3ID / eName - * @returns {Promise} + * Account age in days. + * Derived from the earliest signature timestamp across all binding documents. */ async function fetchAccountAgeDays(eName) { - // TODO: Replace with real evault-core API call - // const res = await fetch(`${EVAULT_CORE_URL}/verification?linkedEName=${eName}`); - // const data = await res.json(); - // const created = new Date(data.createdAt); - // return Math.floor((Date.now() - created.getTime()) / (1000 * 60 * 60 * 24)); - return 0; + const docs = await fetchBindingDocuments(eName); + + let earliest = Infinity; + for (const doc of docs) { + for (const sig of doc.signatures) { + const ts = new Date(sig.timestamp).getTime(); + if (!isNaN(ts) && ts < earliest) { + earliest = ts; + } + } + } + + if (earliest === Infinity) return 0; + return Math.floor((Date.now() - earliest) / (1000 * 60 * 60 * 24)); } /** - * Fetch the user's key storage location from their eVault. - * - * Real implementation will query the eVault GraphQL API for a - * binding document with ontology "KeyStorageInfo" that records - * whether the private key is stored in TPM or SW. - * - * @param {string} eName The user's W3ID / eName - * @returns {Promise<"TPM" | "SW">} + * Key storage location ("TPM" or "SW"). + * Looks for a binding document whose data contains a `keyLocation` field. */ async function fetchKeyLocation(eName) { - // TODO: Replace with real eVault GraphQL query - // const query = `{ metaEnvelopes(filter: { ontology: "KeyStorageInfo" }) { envelopes { value } } }`; - // const res = await fetch(`${EVAULT_DATA_URL}/graphql`, { - // method: 'POST', - // headers: { 'Content-Type': 'application/json', 'X-ENAME': eName }, - // body: JSON.stringify({ query }), - // }); - // const data = await res.json(); - // return data?.data?.metaEnvelopes?.[0]?.envelopes?.[0]?.value?.location ?? 'SW'; + const docs = await fetchBindingDocuments(eName); + + for (const doc of docs) { + const loc = doc.data?.keyLocation || doc.data?.location; + if (loc === "TPM" || loc === "SW") return loc; + } return "SW"; } /** - * Fetch the user's number of 3rd-degree social connections. - * - * Real implementation will query the social graph (to be built - * in the control panel) or aggregate follower/following data - * from the user's eVault MetaEnvelopes across platforms. - * - * @param {string} eName The user's W3ID / eName - * @returns {Promise} + * Number of social connections (1st-degree). + * Counts `social_connection` binding documents that have two signatures + * (i.e. both parties signed). */ async function fetchThirdDegreeConnections(eName) { - // TODO: Replace with real social graph API call - // const res = await fetch(`${CONTROL_PANEL_URL}/api/social-graph/${eName}/third-degree-count`); - // const data = await res.json(); - // return data.count; - return 0; + const docs = await fetchBindingDocuments(eName); + return docs.filter( + (doc) => doc.type === "social_connection" && doc.signatures.length === 2, + ).length; } /** - * Fetch all trust score inputs for a user. - * - * @param {string} eName The user's W3ID / eName - * @returns {Promise<{ isVerified: boolean, accountAgeDays: number, keyLocation: string, thirdDegreeConnections: number }>} + * Fetch all trust-score inputs for a user in a single pass + * (one GraphQL call instead of four). */ async function fetchUserTrustData(eName) { - const [isVerified, accountAgeDays, keyLocation, thirdDegreeConnections] = - await Promise.all([ - fetchVerificationStatus(eName), - fetchAccountAgeDays(eName), - fetchKeyLocation(eName), - fetchThirdDegreeConnections(eName), - ]); + const docs = await fetchBindingDocuments(eName); + + // Verification — at least one id_document + const isVerified = docs.some((doc) => doc.type === "id_document"); + + // Account age — earliest signature timestamp + let earliest = Infinity; + for (const doc of docs) { + for (const sig of doc.signatures) { + const ts = new Date(sig.timestamp).getTime(); + if (!isNaN(ts) && ts < earliest) { + earliest = ts; + } + } + } + const accountAgeDays = + earliest === Infinity + ? 0 + : Math.floor((Date.now() - earliest) / (1000 * 60 * 60 * 24)); + + // Key location + let keyLocation = "SW"; + for (const doc of docs) { + const loc = doc.data?.keyLocation || doc.data?.location; + if (loc === "TPM" || loc === "SW") { + keyLocation = loc; + break; + } + } + + // Social connections + const thirdDegreeConnections = docs.filter( + (doc) => doc.type === "social_connection" && doc.signatures.length === 2, + ).length; return { isVerified, accountAgeDays, keyLocation, thirdDegreeConnections }; } @@ -114,4 +249,11 @@ module.exports = { fetchKeyLocation, fetchThirdDegreeConnections, fetchUserTrustData, + setRegistryUrl, + // Exposed for testing + fetchBindingDocuments, + resolveEVaultUrl, + getPlatformToken, + clearPlatformToken, + normalizeEName, };