From dcf78d009914dea460c2bbd931ab1eb69b50fb08 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 24 Sep 2024 13:08:53 +0100 Subject: [PATCH] Activated accounts only (#1355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Basic site key restrictions and scoring checks * site key registration in pow integration tests * get integration tests working * don't double negative * Move error * link to FAQ when an error shows * Make score optional * lint-fix * Lint-fix * Run admin api in cypress tests * pass timestamps in auth middleware tests * Release 2.1.0 (#1371) * New git branches setup (#1296) * add dev branch to auto merge * make clippy run on new branch setup * remove version checker workflow * add dev and release branch triggers to consistent workflow name checker * Delete create_diagram.yml * add dev and release branch triggers to lint workflow * add release and dev branch triggers to provider_image workflow * remove release drafter * add dev and release branches to rustfmt workflow * add dev and release branches to protocol test workflow * add dev and release branches triggers to tests workflow * add dev and release branch triggers to webpack bundling workflow * switch cache creation to be post pr to dev * rename post_pr workflow to cache * Update cache.yml * publish workflow * Update cache.yml * fix contract js/rust version check workflow * install concurrently and cypress as workspace deps * fix cypress workflow trigger * fix cypress workflow branch triggers * fix rust file triggers * move webpack bundling check to tests workflow * remove typechain version check * rename workflows * make the github actions cache include the branch name * publish on push to main * add js bundle test step * workflow for ensuring all versions move in lockstep * blurb for each workflow * dockerhub login check for before a release * rename publish to release * remove typedoc step * standardise step ids in release workflow * dummy version bump * Delete dockerhub_login.yml * release version check * workflow to check for valid version bumps * auto merge all prs * check consistent workflow names on all prs * branch system docs * check contract version on prs to main,dev,release/* * add workflow_dispatch to all relevant workflows * make auto merge only run on non-draft prs * check versions on all branches * make release run on push to main * tag pushes to main * rename version check workflow * remove fetch all history and branches from clone * fix docker js server version issue * fix bump workflow to bump all package.json files and Cargo.tomls * fix package.json version writing in bump workflow * rename step * make auto merge cancellable * make deploy ahve a single notification of pass/fail * debug info * fix cache workflow name * don't use cache in bump version workflow, not needed * move buildx to top of workflow * move the cli building to before publishing images in deploy workflow * tag warning * remove filters for tests, test more often * add workflow dispatch to deploy workflow * add bash error catching * Create consistent_engine_node.yml * Create consistent_engine_npm.yml * Create consistent_nvmrc.yml * cache docs * event workflows for main and dev * change event setup * don't run unless non-draft pr * combine protocol clippy and rustfmt into single lint workflow * missing engine * missing version * revert version debug * fix version * fix workflow name detection * add missing engines * fix nvmrc check * fix path typo in contract version check * allow auto merge to run always * Update package-lock.json * lint * lint * Cache cypress install (#1305) * remove install cypress step from cache workflow * add cypress as dev dep * Add staging branch to workflow (#1311) * New linting (#1309) * lint dockerfiles * lint caddyfiles * lint github actions workflows * lint shell scripts * yaml lint * lint python * yamllint config * lint js * lint html * lint markdown * htmlhint ignore files * simplify caddy lint * replace eslint + prettier with biome, add run-s and run-p * lint rust * lint toml * configure biome target files * lint svg * jsx and tsx lint * adjust lint command in npm scripts * fix license * change lint changed script name * adjust lint workflow * Update package-lock.json * fix npm script error handling (#1306) * fix auto merge (#1302) * fix auto merge * add repo parameter to auto merge * add repo parameter to auto merge workflow * Remove contract calls (#1308) * remove contract calls from procaptcha * linted stuff * Adding empty providers * Adding hardcoded random providers * Adding changes to load process * Debugging * debug * nearly working state management * fast captchas with no contract * Removing debug * Timestamp signature validation * Working no rpc calls full captcha flow * linting * Unit tests potentially working * dropping debug * All passing cypress demo * Bundle falling over when hashing * dropping bundle pure replace * Remove polkadot manual bundling hacks * No provider pow * Working pow without contract calls * revert sitekey changes * verified instead of context * don't log config * removing cypress capture images * removing bad deps * Removing debug * fixing build * fixing providers * Correct docstring * rebuilt package lock * Removing stats file * Skipping lint in protocol * removing typechain change because protocol bump required * removing unused deps * temp bump while firefighting * Dropping provider gui * Working build without provider gui * Removing batch commit tasks * Dropping protocol * Working build without protocol dir * Dropping proxy and package lock pointing to proxy * Working build with new pkg deps * Removing common contract * Dropping tests for rewrite * Working no captcha product * Dropping merkle tree * Refactoring provider and adding temp tests * Refactored provider into multiple single responsibility classes * lint fix * pkg lock bump * Renaming tests to non test file to pass * Tests passing * Debug for datasetId getter * Working provider set dataset * Auth Middleware Test * Captcha scheduler tests * Error Handler tests * Provider now boots without connection to rpc node * Full suite of unit tests for provider * Tests passing * Fixed build * Integration tests. Actions for this to be set up * Working image captcha integration tests * Unit tests paths correction * Fixing version bump yml * Removing debug * Adding dummy get random provider script * Removing dockerfile and vite env config * Replacing signedTimestamp with timestampSignature * Deduping config by making it its own package * Defaulting message to provider online * Fixing type issues and ws requirement issues --------- Co-authored-by: Chris Taylor Co-authored-by: George Oastler * Removing unused deps, bumping package lock * Working pow captcha with timeouts * Add missing deps and bundling command for staging * Remove version from docker compose files * Use NODE_ENV var for pulling in env var file into docker compose file * Add staging deploy process * Add staging bundle command * package lock * Remove old deps. * Update package-lock.json * Update event name * Add missing dep * Use latest image to create container from * Use staging tag for container * Await crypto WASM blob * Allow NODE_ENV to be passed into Vite config * Add load-balancer refs to tsconfigs * Allow PROSOPO_DEFAULT_ENVIRONMENT to inherit from NODE_ENV * Add some logging and change some imports * exporting hardcoded provider from types * Cleanup linting process (#1312) * lint dockerfiles * lint caddyfiles * lint github actions workflows * lint shell scripts * yaml lint * lint python * yamllint config * lint js * lint html * lint markdown * htmlhint ignore files * simplify caddy lint * replace eslint + prettier with biome, add run-s and run-p * lint rust * lint toml * configure biome target files * lint svg * jsx and tsx lint * adjust lint command in npm scripts * fix license * change lint changed script name * adjust lint workflow * Update package-lock.json * remove old lint scripts * remove prettier + eslint config * remove eslint dep * remove prettier dep * Update package-lock.json * Fix provider not erroring on database connection error. Add option to specify the number of proxies between the provider API and the user * Add staging command back in * deploy staging workflow to bundle branch * addit commit details for bumps * Add watchtower to provider docker compose * Don't mount missing file * watch provider container only * Change container name * Use createConnection instead of connection * Fix build * Staging deploy workflow (#1314) * rename version step * detect prod/staging in deploy * remove contract building in deploy * build js bundles based on env * use docker tag based on env * use prod env * gh release on prod only * npm release on prod only * use docker latest and version specific tags * use docker tags from env in img tags * join version and env steps * prod/dev -> production/development * NODE_ENV variable setup in npm scripts * missing NODE_ENV fixes * fix tsc build / clean cmds * NODE_ENV semis * vite use NODE_ENV as --mode * use NODE_ENV to control bundling of production/development/staging env * use NODE_ENV to control build scripts * remove needless NODE_ENV setting * Delete deploy-staging.yml * deploy on unsafe branch guard * upload bundle to staging release as part of deploy workflow * typo missing vite * license * remove contract linting * remove docker compose linting from dockerfile lint cmd * fix yamllint * make caddy fmt output diff * install htmlhint * fix rustfmt lint script * use cargofmt instead of rustfmt * fix lint rs naming * fix clippy linting * fix toml linting * simplify xargs serial command usage in rust linting * lint rust check * webpack bundle dev mode fix * webpack plugin fix * webpack fix * improve npm scripts * improve node_modules associated with npm scripts * bundle npm script * fix release asset target * remove any current assets before uploading any further staging assets * Pkg json * bump version to v2.0.0 * no build specified * remove deploy_X scripts * remove deploy_X scripts * remove deploy_protocol script call * remove deploy_protocol workflow step * remove github assets for contract * remove deploy_protool * Use 2.0.0 everywhere * Provider tests passing * Adding if present to cjs * Dropping auto merge * Dropping dapp example demo * fix check version bump all history available * Update check_version_bump.yml * Update check_version_bump.yml * Remove out of date readme * Update check_version_bump.yml * Re-add old babel plugins * start provider * Always add the token to the form * Supply token to onHuman function * Fix build * Adding button next for cypress tests * Fixing hanging test * Fixing get dataset being broken * Remove substrate from docker compose * Cypress tests passing * stop the provider workflow from falling over if the npm packages are unreleased * Only load the provider mock container if we built it * Try to upload screenshots * Try to store screenshots on failure * Try using withinSubject:null * Wait for button to be visible * Wait for the the API call * Remove wait for solution request * try to force the click * Just force, don't expect the element * try waiting after clicking * Store videos of cypress runs * Remove the then * Remove duplicate click * Fix function return type * try within * Adding setup step for setup env in test * Add data-cy attributes if environment is not production * wait for docker containers to setup env * Adding setup step for setup env in test * Start docker first * Check if there are references * remove within * Try to kill processes with concurrently * Size fixes for widget and logo * Make signature stuff less confusing * Fix license header positions * Fix tests * Get it all working * Fix size * Fix lint issues (#1318) * rename version step * detect prod/staging in deploy * remove contract building in deploy * build js bundles based on env * use docker tag based on env * use prod env * gh release on prod only * npm release on prod only * use docker latest and version specific tags * use docker tags from env in img tags * join version and env steps * prod/dev -> production/development * NODE_ENV variable setup in npm scripts * missing NODE_ENV fixes * fix tsc build / clean cmds * NODE_ENV semis * vite use NODE_ENV as --mode * use NODE_ENV to control bundling of production/development/staging env * use NODE_ENV to control build scripts * remove needless NODE_ENV setting * Delete deploy-staging.yml * deploy on unsafe branch guard * upload bundle to staging release as part of deploy workflow * typo missing vite * license * remove contract linting * remove docker compose linting from dockerfile lint cmd * fix yamllint * make caddy fmt output diff * install htmlhint * fix rustfmt lint script * use cargofmt instead of rustfmt * fix lint rs naming * fix clippy linting * fix toml linting * simplify xargs serial command usage in rust linting * lint rust check * webpack bundle dev mode fix * webpack plugin fix * webpack fix * improve npm scripts * improve node_modules associated with npm scripts * bundle npm script * fix release asset target * remove any current assets before uploading any further staging assets * add nuke script * set shell in dockerfile * use json form of CMD in dockerfile * fix multiple RUN commands in dockerfile * remove apt sources after install * use apt get * use apt-get in dockerfile instead of apt * join multiple run commands together in dockerfile * entrypoint json form * no install recommends * ignore install recommends lint * fix multiple run commands and apt clear * CMD to json form * no install recommends * remove root user, is default * set nginx version * multiple RUN commands fix * set shell to use pipefail * delete apt lists after install * fix consecutive RUN commands * no install recommends * ignore usage of latest * dockerfile lint * Update substrate-contracts-node.build.dockerfile * json form for CMD * lint dockerfile * lint caddyfile * fix needless cat * useless echo * speech marks for safety * make dockerfile and shell linting command simpler * fix quotes and variable usage in cache workflow * fix quotes * fix quotes * fix quotes * fix quotes * fix NODE_ENV speech marks in workflows * useless cat * unused vars * lint * missing speech marks * speech marks * lint eval and @ usage in shell * lint shell script * fix yamllint linting * fix lint md and js fix cmd * autofix js * fix biome cmdline args * biome lint fixes * biome lint fix * make biome use vcs * no export from tests * fix key in list * missing useEffect dep * explicit any todo * lint fixes * warn on parameter assign * todo for any * trailing comma * ignore any type for options * fix line count parameter reassigned * fix not null assertion * ignore any from axios response * ignore any casts * ignore any in async ctor * no reuse of parameter * ignore i18n translation any * ignore anys in nodejs polars plugin * ignore any for response json * avoid parameter reuse * fix any usage in vite watch plugin * fix side effect in vite watch plugin * fix reused variables in error classes * disable rule for key event with onClick * forEach to for of * missing bracket * allow any * forEach to for of * allow any * forEach to for of * allow any * allow any * forEach to for of * use item hash as key * fix useEffect deps * todo for async exec * allow any + async exec * allow any * forEach to for of * ignore spread * TODOs for sendFunds * allow any * ignore async exec * allow any * todo for memo deps * ignore detector code, contains loads of biome errors * fix anys, foreach and local params * ignore foreach * fix any * allow any * allow any * allow any and foreach * allow any * allow any * allow any * allow enum * allow any * allow any * allow any * allow any * allow any * allow any * allow any * allow any * allow any * allow any * allow any * allow any * allow any * foreach to for of * allow any * allow any * allow any and foreach to for of * img buffer type * allow any * allow any * allow any * foreach to for of * allow any * make biome do formatting too * biome format * ignore bundles * lint * lint md files * lint md * disable some markdown rules * lint rs * fix rs * fix npm scripts * lint * ignore ruff.toml when linting tomls * lint * lint py npm scripts * remove eslint directives * make biome track the main branch and handle css * fix caddy linting * lint * lint * lint * configure taplo * Update ruff.toml * fix scripts * lint * remove eslint * lint * fix speech mark issues * typo * fix lint fixes types * more fixes for linting fixes * fix for potentially undefined root node of merkle tree * duplicate selector * lint * ignore bundle stats html file * missing provider env for starting provider * async factory fix * remove debug * lint * revert detector index.js changes * lint * polyfill node: imports * fix for node:url breaking bundle * use package workspaces rather than cd'ing (#1321) * Add push to js branch again to get live staging bundle * Remove any existing procaptcha responses on human * deploy staging on push to staging * Use duplicate JS Server for now * Silence GH warning about NoSQL injection in demo * Silence GH warning about NoSQL injection in demo * Set GH_TOKEN env * Remove production step for JS Server * Add back command to push to js-bundle branch. Remove quotes in asset deletion script * Use correct docker tag * Try moving to its own step * don't fail if nothing to commit * Update provider docker run CMD * Always pull new version of tag for provider image * Update provider CMD * Alter the verify endpoint when in non-production environments in client-example demo * Allow PoW captchas to be stored and counted * Removing exportData from Manager * Adding synamic config for load balancer * Fix build * fix server build * fix server again * fix provider build * Removing dangerou * Change scale-ts type to u64 to avoid losing data * revert change to number for timestamp * make sure procaptcha-response is deleted * Fix scheduler database name and add env option for storage schedule * Make the models available once the connection has been made * Move headers * Update js_server release step * Try using secret directly * Add some debug * Modify debug * Yet more debug * I give up * Working captcha results in mongo (#1322) * Fixing db schema for pow captchas * dropping new debug * Use a var for mongo image * more debug * Removing lint fix from version bump * User data mvp (#1327) * Update captcha records for user portal data requirements * pow types overhaul * Harden tests against function parameter changes * only run prosoponator on non-draft pr's (#1333) * fix dir capitalisation (#1329) * fix dir capitalisation * fix path * Introduce dotenv package for finding environment files (#1334) * Introduce dotenv package for finding environment files * Package-lock.json * Update references to old loadEnv * Update package references * Add the config files * Update loadEnv ref * Update loadEnv refs in demos * trigger tests * Update dotenv reference * Specify no tests * Fix dataset IDs for staging providers * Allow pow challenges to be submitted more than once for verification * Fail if the challenge exists in the db * Revert unlimited challenge checking * Make demo re-render captcha component when form changes * Allow navigating between the captcha types * Fix license location * remove debug * Store something on each request. Update PoW types. Additional checks (#1336) * removed network types * remove substrate containers / env params * remove CONTRACT env vars * fix imports * remove networks * remove network from getPairAsync * remove networks + contract * remove default network from env * remove default network * remove network default + types from types pkg * remove network types * remove block number * Update package-lock.json * Update package-lock.json * remove contract dep for demo * Store something on each request. Update PoW types. Additional checks * Additional checks * Restrict captcha solution checking by dapp account * fix build * Fix cypress tests * lots of updates to types * Fix up tests. Add types for Get Image Captcha URL * Remove some unuse types. Type an API URL * Zod type translation keys. Use enum values when updating database. Add enum arrays to mongoose schemas. Use ApiParams everywhere * Set the correct key and value for storedStatus * Fix up unit tests * Make sure we're setting updated timestamps everywhere. Don't send the same data to the captcha storage DB twice. Add details of cron jobs that have run to scheduled tasks collection * Fix up tests * remove file that does nothing --------- Co-authored-by: George Oastler * Bug fix for missing status in scheduled task record * Fix captcha storage mechanism. Add tests (#1337) * Fix captcha storage mechanism. Add tests * Fix up tests. Move verifyRecency function to util * Update test env template * Fix build * use correct branch name in bump version * Fix captcha scheduling insert (#1341) * Stop scheduler from trying to set ID on upsert * package-lock.json * change logging * Add types to tests to filter by test type during testing (#1342) * vite test type env parameter * prepend test type to all test files * always include tests with no type specified * Fix last scheduled task getter (#1346) Change the db method and add some unit tests around scheduled tasks * Lint repo (#1349) * JS in repo linted * addince licences * Removing unused linting * run lint in ci container * GHA fix * js server shell script linting * yaml lint fix * pkg lock lint --------- Co-authored-by: George Oastler * Docker compose overhaul (#1350) * fix NODE_ENV var in arm64 version of provider compose * add restart policy to docker compose * add watchtower to arm64 * add pull policy to arm64 * make watchtower cleanup old imgs * watchtower remove anon vols * watchtower log format * only restart enabled containers * ping slack when restarts happen * only restart enabled containers * ping slack when updates occur * docs * make env based db image * combine arm64 and x86_64 provider compose files * mongodb healthcheck * healthcheck for caddy * healthcheck for provider * disable caddy fileserver * metrics in caddyfile * add env vars to caddyfile * basicauth for /metrics * add env vars for uname + pwd of basicauth * docker compose profiles * remove tty * add env file to caddy * provider-dev service * make database-dev use env file * remove watchtower env params * add watchtower env file * watchtower logging * watchtower restart policy * watchtower self restart * add missing env params * remove dev version of provider compose file * usage doc for caddy * move env vars * sort env lines * prepend caddy env parameters with CADDY_ * add provider image version env parameter * add missing env params * add missing template env vars * drop rococo env * move package version env var * more old space size for testing * remove name from watchtower * configure watchtower via env * remove watchtower healthcheck * Delete Dockerfile * Caddyfile defaults (#1352) * add caddy provider name + port vars * sane defaults for provider container name + port in caddyfile * sane default for http port * remove http port for prod * sane default for https * remove https var for prod * missing https var * caddy admin endpoint default * remove admin endpoint var * disable caddy metrics (not in use atm) * disable metrics reverse proxy * log to stdout as well * fix provider endpoint vars * fix defaults * rimraf dep (#1331) * rimraf dep * Update package-lock.json * security policy (#1343) * Portal settings (#1351) * wip for client settings retrieval. database overhaul * types update * Add basic domain validator * big domain regex * lint-fix * Add test class * install missing dep * bump polyfill plugin * package-lock.json * limit domain length * package locl * lint fix * drop logs * Address PR comments * TLS headers (#1353) * Create tls.Caddyfile * add headers to express / types / tests * pass headers * add headers to mongo * lint * move tls headers from debug caddyfile * fix missing headers in types * fix missing headers * lint * lint * fix error of unknown type * lint * disable gh actions lint * lint * remove old demo/example pkgs (#1345) * remove old demo/example pkgs * pow in example * remove legacy npm scripts * Provider retry (#1348) * pkg lock bump and manager logic for retry * Dev cli with multiple providers and provider faliure fallover * Biome fixes * pkg json lint * Provider retry in pow from common * Non cached s3 provider getting * working retryCount * pgk lock bump * removing linting junk * Working build and cleaner dev cli * Removing unused debug * pkg lock bump for deps * adding types glob * Dropping load balancer hardcoded jsons * Removing non env based dev provider start * use node env for dev mode * Properly logging final retry faliure * pkg lock bump * package lock is locked up :joy: * pkg lock lock lock * Linting * Lint * marking lint actions as disabled * Only 1 provider * :lock: * :lock: * :lock: * remove glob and lint * 📦🔒 * pin vite to 5.4.2 * 📦🔒 * Glob stink * globSync * lint fix * bump vitest and vite * add vite and vitest to root pkg json * remove vitest cov * Update package-lock.json * fix exclude for vite test config * 📦🔒 * vitest/coverage-v8 * use fast-glob * lint-fix * remove lint:yaml command * Lint-fix:js --------- Co-authored-by: Chris Taylor Co-authored-by: George Oastler * remove find-up (#1354) * Stop npm from failing on extraneous packages * Add package-lock-only flag * drop depends * Deps (#1356) * drop unneeded workspace deps * fix deps provider-mock * fix deps cypress-shared * fix deps client-example * fix deps client-example-server * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * dep * deps * deps * deps * deps * deps * deps * fix import * fix import * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * dep * npm-check * pkg lock * webpack cli * Update package-lock.json * lint * webpack deps * Update package-lock.json * lint * vitest coverage * Update package-lock.json * revert procaptcha-bundle + config pkg deps * order * Update package-lock.json * add standard devdeps to all pkgs * Update package-lock.json * tsx * Update package-lock.json * lint * Deploy bundle on push (#1362) Deploying bundle to s3 * No hats (#1357) * drop unneeded workspace deps * fix deps provider-mock * fix deps cypress-shared * fix deps client-example * fix deps client-example-server * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * dep * deps * deps * deps * deps * deps * deps * fix import * fix import * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * dep * npm-check * pkg lock * remove hats * purge hats * Update package-lock.json * bump pkg vers * fix array access errors for new ts version * bump i18n * bump pkg vers * fix array access for new ts ver * fix dep vers * Update package-lock.json * lint * remove hat * add webpack and rollup plugins back in * Update package-lock.json * bump rollup and webpack plugin vers * missing babel plugins * bump babel deps * Update package-lock.json * move import * add comments for biome ignores * npmrc (#1361) * Adding prosopo to css selectors (#1363) Adding prosopo- to all classnames Co-authored-by: George Oastler * Deps (#1365) * drop unneeded workspace deps * fix deps provider-mock * fix deps cypress-shared * fix deps client-example * fix deps client-example-server * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * dep * deps * deps * deps * deps * deps * deps * fix import * fix import * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * deps * dep * npm-check * pkg lock * remove hats * purge hats * Update package-lock.json * bump pkg vers * fix array access errors for new ts version * bump i18n * bump pkg vers * fix array access for new ts ver * fix dep vers * Update package-lock.json * lint * remove hat * add webpack and rollup plugins back in * Update package-lock.json * bump rollup and webpack plugin vers * missing babel plugins * bump babel deps * Update package-lock.json * move import * add comments for biome ignores * add devdeps * more devdeps * consolidate pkg vers * drop hats * drop hats * drop npm check * Update package-lock.json * bump deps * scripts * remove emotion react * ignore standard pkgs from depcheck * typo * Update package-lock.json * lint * npmrc fix (#1366) Co-authored-by: Chris * bump vite to 5.4.6 * 📦🔒 * fix bump version workflow (#1368) * Bump version to 2.1.0 * Bump to 2.0.1 * Lint-fix * Bump env versions * saving to js folder (#1373) saving t o js folder * Adding spanish and portuguese support (#1364) * Working build for ES and PT language support * Auto update lang in all captcha types * Working i18n * All translations * Dropping empty file * Linting and formatting * Adding tests and refactoring bundle * package lock bumps * proper numbers :sparkle: * :lock: * lint fix * Linting fixes * more linting aa * License formatting * Add config for resolving .js extensions in ESM context in webpack build * lint-fix * Add check for build and build:cjs * check that number of references in tsconfig.json equals the number in tsconfig.cjs.json * Stop tsconfigs from having missing references * Fix script for comparing package.jsons and tsconfig.json * fix all missing packages * 📦🔒 * lint-fix * fix broken package version * add missing ref * Make sure file does not exist * lint-fix * Script to remove unused JSON keys * Remove ununsed JSON keys * lint-fix * remove PROGUI section --------- Co-authored-by: Chris Taylor * Package bumps * recursive copy * caddyfile defaults for variables * Copy top level files only * deploy fix (#1374) * deploy fix * remove demo changes * dumbb lint * english language * Use inherited vite config from dev/config package (#1376) * Use inherited vite config from dev/config package * lint-fix * Add watch workspace plugin back in * i18n fixes (#1377) * i18n fixes * Removing unused lang, linting, etc * lint * Portugese Brazilian (#1379) * Portugese Brazilian * Add pt-BR to zod enum --------- Co-authored-by: Hugh * Commit so that i can pin --------- Co-authored-by: George Oastler Co-authored-by: George Oastler Co-authored-by: Chris Co-authored-by: Hugh <90424587+HughParry@users.noreply.github.com> Co-authored-by: Hugh * Apply code scanning fix for information exposure through a stack trace Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Sort out test config * Don't send stacks via API * lint-fix * Remove argument level * Add command to start provider with admin api * Change command to work with concurrently * Update correct workflow * Move imports * 📦🔒 * lint-fix * Bundle docs URL * Move translation strings * Add test to catch translation keys missing * Big locale overhaul * Big locale overhaul * 📦🔒 * 📦🔒 * 📦🔒 * tsconfig refs * 📦🔒 * remove polkadot/util from common * Use correct package name * Use correct package name * lint-fix * package dir getters * Lint-fix and fix import * Address PR comments * specify no tests for the locale-browser package * More imports placed below license * Lint-fix * remove rogue npmignore --------- Co-authored-by: prosoponator <156670514+prosoponator@users.noreply.github.com> Co-authored-by: George Oastler Co-authored-by: George Oastler Co-authored-by: Hugh <90424587+HughParry@users.noreply.github.com> Co-authored-by: Hugh Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/bump_version.yml | 3 + .github/workflows/cypress.yml | 6 +- .github/workflows/deploy.yml | 337 +++++++++++++++ .github/workflows/tests.yml | 56 +++ demos/client-example/vite.config.ts | 3 + demos/cypress-shared/cypress.config.js | 7 + .../cypress-shared/cypress/e2e/captcha.cy.ts | 22 +- .../cypress/e2e/correct.captcha.cy.ts | 22 +- .../cypress/e2e/correct.captcha.signup.cy.ts | 22 +- dev/config/src/vite/vite.frontend.config.ts | 3 + package.json | 1 + packages/api/src/api/ProviderApi.ts | 6 +- packages/cli/package.json | 1 + packages/cli/src/RateLimiter.ts | 8 +- packages/cli/src/argv.ts | 2 + packages/cli/src/commands/index.ts | 1 + packages/cli/src/commands/siteKeyRegister.ts | 53 +++ packages/cli/src/commands/validators.ts | 8 + packages/common/src/locales/en.json | 407 ++++++++++++++++++ packages/common/src/locales/es.json | 398 +++++++++++++++++ packages/common/src/locales/pt-BR.json | 398 +++++++++++++++++ packages/common/src/locales/pt.json | 398 +++++++++++++++++ packages/common/src/logger.ts | 6 + packages/common/src/node/UrlConverter.ts | 327 ++++++++++++++ packages/database/src/databases/provider.ts | 17 +- packages/locale/src/locales/en.json | 6 +- packages/locale/src/locales/es.json | 4 +- packages/locale/src/locales/pt-BR.json | 4 +- packages/locale/src/locales/pt.json | 4 +- packages/locale/src/node/i18nMiddleware.ts | 1 + .../procaptcha-bundle/src/util/language.ts | 3 +- .../procaptcha-common/src/state/builder.ts | 7 +- .../src/components/ProcaptchaWidget.tsx | 1 + .../procaptcha-pow/src/services/Manager.ts | 99 +++-- .../src/components/ProcaptchaWidget.tsx | 5 +- packages/procaptcha/src/modules/Manager.ts | 56 +-- .../src/modules/ProsopoCaptchaApi.ts | 23 +- packages/provider/src/api/admin.ts | 33 +- packages/provider/src/api/authMiddleware.ts | 27 +- packages/provider/src/api/captcha.ts | 81 +++- .../provider/src/tasks/client/clientTasks.ts | 8 + .../src/tasks/imgCaptcha/imgCaptchaTasks.ts | 1 + .../provider/src/tasks/powCaptcha/powTasks.ts | 1 + .../imgCaptcha.integration.test.ts | 40 +- .../powCaptcha.integration.test.ts | 93 +++- .../src/tests/integration/registerSitekey.ts | 35 ++ .../unit/api/authMiddleware.unit.test.ts | 12 +- packages/provider/vite.test.config.ts | 18 +- packages/types-database/src/types/mongo.ts | 2 +- packages/types-database/src/types/provider.ts | 2 + packages/types/src/api/params.ts | 2 + packages/types/src/config/config.ts | 1 + packages/types/src/datasets/captcha.ts | 1 + packages/types/src/procaptcha/api.ts | 14 +- packages/types/src/procaptcha/manager.ts | 5 +- packages/types/src/provider/api.ts | 34 +- packages/util/src/util.ts | 4 +- packages/web-components/src/Checkbox.tsx | 56 ++- packages/web-components/src/theme.ts | 7 + 59 files changed, 2984 insertions(+), 218 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 packages/cli/src/commands/siteKeyRegister.ts create mode 100644 packages/common/src/locales/en.json create mode 100644 packages/common/src/locales/es.json create mode 100644 packages/common/src/locales/pt-BR.json create mode 100644 packages/common/src/locales/pt.json create mode 100644 packages/common/src/node/UrlConverter.ts create mode 100644 packages/provider/src/tests/integration/registerSitekey.ts diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml index f43eb9fd51..b0ded91aba 100644 --- a/.github/workflows/bump_version.yml +++ b/.github/workflows/bump_version.yml @@ -28,6 +28,9 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true + +env: + GITHUB_TOKEN: ${{ secrets.PROSOPONATOR_PAT }} env: GITHUB_TOKEN: ${{ secrets.PROSOPONATOR_PAT }} diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 0281064f14..9cdb5aba4d 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -118,15 +118,15 @@ jobs: - name: Run the cypress tests on client-example run: | - npx concurrently "npm run start:server" "npm run start:provider" "npm run start:demo" "sleep 10s && npm -w @prosopo/cypress-shared run cypress:run:client-example" --success "first" --kill-others + npx concurrently "npm run start:server" "npm run start:provider:admin" "npm run start:demo" "sleep 10s && npm -w @prosopo/cypress-shared run cypress:run:client-example" --success "first" --kill-others - name: Run the cypress tests on client-bundle-example run: | - npx concurrently "npm run start:server" "npm run start:provider" "npm run start:bundle" "sleep 10s && npm -w @prosopo/cypress-shared run cypress:run:client-bundle-example" --success "first" --kill-others + npx concurrently "npm run start:server" "npm run start:provider:admin" "npm run start:bundle" "sleep 10s && npm -w @prosopo/cypress-shared run cypress:run:client-bundle-example" --success "first" --kill-others - name: Run the cypress tests on client-bundle-example explicit rendering run: | - npx concurrently "npm run start:server" "npm run start:provider" "npm run start:bundle" "sleep 10s && npm -w @prosopo/cypress-shared run cypress:run:client-bundle-example:explicit" --success "first" --kill-others + npx concurrently "npm run start:server" "npm run start:provider:admin" "npm run start:bundle" "sleep 10s && npm -w @prosopo/cypress-shared run cypress:run:client-bundle-example:explicit" --success "first" --kill-others # after the test run completes store videos and any screenshots - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..bca833152d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,337 @@ +# Publish / release / deploy packages after a release + +name: deploy + +on: + push: + branches: [main, staging] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Print contexts + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + ENV_CONTEXT: ${{ toJson(env) }} + VARS_CONTEXT: ${{ toJson(vars) }} + JOB_CONTEXT: ${{ toJson(job) }} + STEPS_CONTEXT: ${{ toJson(steps) }} + RUNNER_CONTEXT: ${{ toJson(runner) }} + SECRETS_CONTEXT: ${{ toJson(secrets) }} + STRATEGY_CONTEXT: ${{ toJson(strategy) }} + MATRIX_CONTEXT: ${{ toJson(matrix) }} + NEEDS_CONTEXT: ${{ toJson(needs) }} + INPUTS_CONTEXT: ${{ toJson(inputs) }} + run: | + echo "******************************" + echo "github:" "$GITHUB_CONTEXT" + echo "******************************" + echo "env:" "$ENV_CONTEXT" + echo "******************************" + echo "vars:" "$VARS_CONTEXT" + echo "******************************" + echo "job:" "$JOB_CONTEXT" + echo "******************************" + echo "steps:" "$STEPS_CONTEXT" + echo "******************************" + echo "runner:" "$RUNNER_CONTEXT" + echo "******************************" + echo "secrets:" "$SECRETS_CONTEXT" + echo "******************************" + echo "strategy:" "$STRATEGY_CONTEXT" + echo "******************************" + echo "matrix:" "$MATRIX_CONTEXT" + echo "******************************" + echo "needs:" "$NEEDS_CONTEXT" + echo "******************************" + echo "inputs:" "$INPUTS_CONTEXT" + echo "******************************" + + # Add support for more platforms with QEMU (optional) + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Dockerhub login + run: | + echo "Logging into Docker Hub." + echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 2> /dev/null + + - uses: actions/checkout@v3 + + - name: Detect env + id: env + run: | + + set -euxo pipefail # stop on errors, print commands, fail on pipe fails + + VERSION=$(jq -r '.version' < package.json) + + # if version not detected, error + if [[ -z "$VERSION" ]]; then + echo "Failed to get version" + exit 1 + fi + # or if version null + if [[ "$VERSION" == "null" ]]; then + echo "Failed to get version" + exit 1 + fi + + echo "version=$VERSION" + + # export the next version numbers + # shellcheck disable=SC2086 + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # if branch is main, then prod, else staging + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "production" + # shellcheck disable=SC2086 + { + echo "env=production"; + echo "production=true"; + echo "docker_latest_tag=latest"; + echo "docker_version_tag=$VERSION" + } >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == "refs/heads/staging" ]]; then + echo "staging" + # shellcheck disable=SC2086 + { + echo "env=staging"; + echo "staging=true"; + echo "docker_latest_tag=staging"; + echo "docker_version_tag=staging" + } >> $GITHUB_OUTPUT + else + echo "cannot deploy from branch ${{ github.ref }}" + exit 1 + fi + + - uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - run: npm i -g "npm@$(jq -r .engines.npm < package.json)" + + - run: npm ci + + # build from scratch to ensure nothing bought over from cache + + - name: Build packages + run: | + echo "Building packages..." + npm run build:all + npm run build:all:cjs + + - name: Build JS bundle + run: | + set -euxo pipefail # stop on errors, print commands, fail on pipe fails + + # Copy the rococo env file to production env file + echo "Copying the rococo env to production env file in procaptcha-bundle" + cp ./dev/scripts/env.${{ steps.env.outputs.env }} ./packages/procaptcha-bundle/.env.${{ steps.env.outputs.env }} + + # Navigate to the JS bundle directory and build + echo "Building JS bundle..." + + NODE_ENV="${{ steps.env.outputs.env }}" npm -w @prosopo/procaptcha-bundle run bundle + + - name: Push the JS Bundle to the js-bundle branch + run: | + set -euxo pipefail # stop on errors, print commands, fail on pipe fails + # Checkout js-bundle branch + git fetch origin js-bundle:js-bundle + git checkout js-bundle + + # Copy built bundle to js-bundle branch + cp $(find packages/procaptcha-bundle/dist/bundle/* -maxdepth 0 -not -type d) . + + # set the author in git + git config user.name "prosoponator[bot]" + git config user.email "dev@prosopo.io" + + git add -A + git commit -m 'Deploy JS bundle from staging' || true + + # Push the bundle to js-bundle branch + git push origin js-bundle --force + + # Return to the original branch + git checkout ${{ github.ref }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-1 + + - name: Deploy bundle to AWS S3 + if: ${{ steps.env.outputs.production || steps.env.outputs.staging }} + run: | + if [ "${{ steps.env.outputs.production }}" = "true" ]; then + aws s3 cp packages/procaptcha-bundle/dist/bundle/ s3://js.prosopo.io/js/ --recursive + elif [ "${{ steps.env.outputs.staging }}" = "true" ]; then + aws s3 cp packages/procaptcha-bundle/dist/bundle/ s3://staging-js.prosopo.io/js/ --recursive + fi + + - name: Build docker js_server + run: | + set -euxo pipefail # stop on errors, print commands, fail on pipe fails + + # technically, if the release is for a version which is less than a published version, the following code will have bundles from versions ahead of this one. However, this isn't really a problem, as we just want the latest bundle to be available alongside old ones, not so bothered about newer ones. + + # Set the JS location in the container + JS_FOLDER="/usr/share/nginx/html/js" + + # Get the most recent version of the js_server image + docker pull "prosopo/js_server:${{ steps.env.outputs.docker_latest_tag }}" + + # Create a temporary container from the latest image + echo "Building Docker image..." + OLD_CONTAINER_ID="$(docker create "prosopo/js_server:${{ steps.env.outputs.docker_latest_tag }}")" + + # Remove the old js temp folder + rm -rf ./js_bundles_host_temp + + # Copy out the old files + docker cp "$OLD_CONTAINER_ID:$JS_FOLDER" ./js_bundles_host_temp + + # Build the new image + docker build --file ./docker/images/js.server.dockerfile . -t "prosopo/js_server:${{ steps.env.outputs.docker_version_tag }}" --no-cache + + # Run the new image + NEW_CONTAINER_ID="$(docker create "prosopo/js_server:${{ steps.env.outputs.docker_version_tag }}")" + + # Copy the legacy files across + docker cp ./js_bundles_host_temp/ "$NEW_CONTAINER_ID:$JS_FOLDER/" + + # Copy the new bundle files to the container into a folder with the version name + docker cp packages/procaptcha-bundle/dist/bundle/. "$NEW_CONTAINER_ID:$JS_FOLDER" + + # Start the new container + docker start "$NEW_CONTAINER_ID" + + # Move procaptcha.bundle.js + docker exec "$NEW_CONTAINER_ID" mv "$JS_FOLDER/procaptcha.bundle.js" "$JS_FOLDER/procaptcha.bundle.${{ steps.env.outputs.docker_version_tag }}.js" + + # Symlink JS_FOLDER/procaptcha.bundle.js to JS_FOLDER/procaptcha.bundle.VERSION.js + docker exec "$NEW_CONTAINER_ID" ln -sf "$JS_FOLDER/procaptcha.bundle.${{ steps.env.outputs.docker_version_tag }}.js" "$JS_FOLDER/procaptcha.bundle.js" + + # Commit the changes to the container + docker commit "$NEW_CONTAINER_ID" "prosopo/js_server:${{ steps.env.outputs.docker_version_tag }}" + + # Check this new docker image works locally + docker run -d -p 3080:80 "prosopo/js_server:${{ steps.env.outputs.docker_version_tag }}" + + # Start the bundle demo & run the cypress tests against the new bundle + # npx concurrently "npm run start:bundle" "npm run -w @prosopo/cypress-shared cypress:run:client-bundle-example:js_server" --success "first" --kill-others + + - name: Build the production CLI package + run: | + set -euxo pipefail # stop on errors, print commands, fail on pipe fails + + echo "Building the production CLI package..." + # Copy the rococo env file to production env file + echo "Copying the rococo env to production env file in cli package" + cp ./dev/scripts/env.${{ steps.env.outputs.env }} ./packages/cli/.env.${{ steps.env.outputs.env }} + + # Navigate to the provider CLI directory and build + NODE_ENV="${{ steps.env.outputs.env }}" npm run -w @prosopo/cli build + NODE_ENV="${{ steps.env.outputs.env }}" npm run -w @prosopo/cli bundle + + # this will error if there's already a release for this version. This is by design, if you need to fix a release (e.g. because a deploy failed) then bump the version and try again + - name: Github release + id: github_release + if: ${{ steps.env.outputs.production }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # do a gh release with the contract sources + bundle + gh release create --generate-notes "v${{ steps.env.outputs.version }}" "./packages/procaptcha-bundle/dist/bundle/procaptcha.bundle.js" + + - name: Npm release + if: ${{ steps.env.outputs.production }} + run: | + if [[ "${{ vars.HOST_GITHUB_ACTIONS }}" == true ]]; then + echo "Running on GitHub Actions" + # Write the npm token to ~/.npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + echo "Publishing to npm dry-run..." + npm run publish:dry-run + echo "Publishing to npm..." + npm run publish + else + echo "Running locally via act, skipping npm publish." + fi + + - name: Docker js_server release + id: docker_js_server_release + run: | + # Push the new image to Docker Hub + echo "Pushing Docker image..." + docker push prosopo/js_server:${{ steps.env.outputs.docker_version_tag }} + + # Push latest + docker tag prosopo/js_server:${{ steps.env.outputs.docker_version_tag }} prosopo/js_server:${{ steps.env.outputs.docker_latest_tag }} + docker push prosopo/js_server:${{ steps.env.outputs.docker_latest_tag }} + + - name: Get Prosopo JS Server Flux App Name + run: | + # shellcheck disable=SC2086 + { + echo "JS_SERVER_FLUX_APP_NAME=${{ steps.env.outputs.env }}_JS_SERVER_FLUX_APP_NAME" | tr '[:lower:]' '[:upper:]'; + } >> $GITHUB_ENV + + - name: echo github env + run: | + echo "GITHUB_ENV: $GITHUB_ENV" + + - name: Print the JS_SERVER name + env: + VAR_NAME: ${{ env.JS_SERVER_FLUX_APP_NAME }} + JS_SERVER_FLUX_APP_NAME: ${{ secrets[env.JS_SERVER_FLUX_APP_NAME] }} + run: | + echo "JS_SERVER_FLUX_APP_NAME: $JS_SERVER_FLUX_APP_NAME" + echo "VAR NAME: $VAR_NAME" + echo "JS_SERVER_FLUX_APP_NAME: $JS_SERVER_FLUX_APP_NAME" + + - name: Redeploy flux docker js_server + env: + PROSOPO_ZELCORE_PRIVATE_KEY: ${{ secrets.PROSOPO_ZELCORE_PRIVATE_KEY }} + PROSOPO_ZELCORE_PUBLIC_KEY: ${{ secrets.PROSOPO_ZELCORE_PUBLIC_KEY }} + run: | + if [[ "${{ steps.docker_js_server_release.outcome }}" == 'success' ]]; then + echo "Installing @prosopo/flux..." + npm i -g @prosopo/flux + echo "Hard redeploying flux docker js_server: ${{ secrets[env.JS_SERVER_FLUX_APP_NAME] }}" + npx flux redeploy ${{ secrets[env.JS_SERVER_FLUX_APP_NAME] }} --hard + else + echo "Skipping flux redeploy." + fi + + - name: Build and push the Provider Container + uses: docker/build-push-action@v5 + with: + context: ${{github.workspace}} + file: ${{github.workspace}}/docker/images/provider.dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: prosopo/provider:${{ steps.env.outputs.docker_version_tag }},prosopo/provider:${{ steps.env.outputs.docker_latest_tag }} + + - name: Deploy fail notification + if: failure() + run: | + echo "Deploy failed" + curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":check-failed: Deploy <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ steps.env.outputs.env }} v${{ steps.env.outputs.version }}> failed.\"}" ${{ secrets.SLACKBOT_DEVOPS }} + + - name: Deploy success notification + if: success() + run: | + echo "Deploy succeeded" + curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":check-passed: Deploy <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ steps.env.outputs.env }} v${{ steps.env.outputs.version }}> succeeded.\"}" ${{ secrets.SLACKBOT_DEVOPS }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fb191f6f8e..3619c9af75 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -136,6 +136,62 @@ jobs: cat missing_references.txt fi + # check that each package.json contains a `build` and `build:cjs` script. + - name: Check for build and build:cjs scripts + run: | + for file in $(find ./packages -name package.json -not -path "**/node_modules/*" -not -path "./packages/detector/*"); do + if ! grep -q '"build":' $file; then + echo "Missing 'build' script in $file" + exit 1 + fi + if ! grep -q '"build:cjs":' $file; then + echo "Missing 'build:cjs' script in $file" + exit 1 + fi + done + + # check that the number of references in each tsconfig.json is equal to the number of references in the tsconfig.cjs.json for each package + - name: Check for matching references in tsconfig.json and tsconfig.cjs.json + run: | + rm -f mismatched_references.txt + for file in $(find ./packages -name tsconfig.json -not -path "**/node_modules/*" -not -path "./packages/detector/*"); do + if [ $(jq '.references | length' $file) -ne $(jq '.references | length' $(dirname $file)/tsconfig.cjs.json) ]; then + echo "Mismatched number of references in $file and $(dirname $file)/tsconfig.cjs.json" >> mismatched_references.txt + fi + done + if [ -f mismatched_references.txt ]; then + cat mismatched_references.txt + exit 1 + fi + + # check that each @prosopo package referenced in package.json is in the tsconfig.json for each package + - name: Check for matching references in package.json and tsconfig.json + run: | + rm -f missing_references.txt + for file in $(find ./packages -name package.json -not -path "**/node_modules/*" -not -path "./packages/detector/*"); do + for package in $(jq -r '.dependencies | keys[]' $file); do + # if the package is not a @prosopo package, skip it + if ! echo $package | grep -q "^@prosopo"; then + continue + fi + + # if the package is @prosopo/config, skip it + if [[ "$package" == "@prosopo/config" ]]; then + continue + fi + + # split the package by '/' and get the last element + searchterm=$(echo $package | awk -F/ '{print $NF}') + if ! grep -oI "\"..\/$searchterm\"" $(dirname $file)/tsconfig.json; then + # store the results + echo "Missing reference to $searchterm in $(dirname $file)/tsconfig.json" >> missing_references.txt + fi + done + done + if [ -f missing_references.txt ]; then + cat missing_references.txt + fi + - uses: actions/setup-node@v3 with: node-version-file: '.nvmrc' diff --git a/demos/client-example/vite.config.ts b/demos/client-example/vite.config.ts index 0265e4e72d..69d17a076b 100644 --- a/demos/client-example/vite.config.ts +++ b/demos/client-example/vite.config.ts @@ -57,6 +57,9 @@ export default defineConfig(async ({ command, mode }) => { "process.env.PROSOPO_LOG_LEVEL": JSON.stringify( process.env.PROSOPO_LOG_LEVEL || "Info", ), + "process.env.PROSOPO_DOCS_URL": JSON.stringify( + process.env.PROSOPO_DOCS_URL, + ), }, build: { outDir: path.resolve("./dist"), diff --git a/demos/cypress-shared/cypress.config.js b/demos/cypress-shared/cypress.config.js index 6fa187f540..740a54a99e 100644 --- a/demos/cypress-shared/cypress.config.js +++ b/demos/cypress-shared/cypress.config.js @@ -13,12 +13,19 @@ import vitePreprocessor from "cypress-vite"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import { loadEnv } from "@prosopo/dotenv"; +import { defineConfig } from "cypress"; +import vitePreprocessor from "cypress-vite"; import { nodePolyfills } from "vite-plugin-node-polyfills"; +loadEnv(); + export default defineConfig({ video: true, headers: { "Accept-Encoding": "gzip, deflate" }, env: { + ...process.env, default_page: "/", }, e2e: { diff --git a/demos/cypress-shared/cypress/e2e/captcha.cy.ts b/demos/cypress-shared/cypress/e2e/captcha.cy.ts index fad6b6fefd..a28856b5aa 100644 --- a/demos/cypress-shared/cypress/e2e/captcha.cy.ts +++ b/demos/cypress-shared/cypress/e2e/captcha.cy.ts @@ -13,13 +13,33 @@ // limitations under the License. /// import "@cypress/xpath"; +import { u8aToHex } from "@polkadot/util"; import { ProsopoDatasetError } from "@prosopo/common"; +import { getPairAsync } from "@prosopo/contract"; import { datasetWithSolutionHashes } from "@prosopo/datasets"; -import type { Captcha } from "@prosopo/types"; +import { AdminApiPaths, type Captcha } from "@prosopo/types"; import { at } from "@prosopo/util"; import { checkboxClass } from "../support/commands.js"; describe("Captchas", () => { + before(async () => { + const timestamp = new Date().getTime(); + const pair = await getPairAsync(Cypress.env("PROSOPO_PROVIDER_MNEMONIC")); + const signature = u8aToHex(pair.sign(timestamp.toString())); + const adminSiteKeyURL = `http://localhost:9229${AdminApiPaths.SiteKeyRegister}`; + await fetch(adminSiteKeyURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + signature: signature, + timestamp: timestamp.toString(), + }, + body: JSON.stringify({ + siteKey: Cypress.env("PROSOPO_SITE_KEY"), + }), + }); + }); + beforeEach(() => { const solutions = datasetWithSolutionHashes.captchas.map((captcha) => ({ captchaContentId: captcha.captchaContentId, diff --git a/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts b/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts index f3000a9866..2f424aaea4 100644 --- a/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts +++ b/demos/cypress-shared/cypress/e2e/correct.captcha.cy.ts @@ -13,12 +13,32 @@ // limitations under the License. /// import "@cypress/xpath"; +import { u8aToHex } from "@polkadot/util"; import { ProsopoDatasetError } from "@prosopo/common"; +import { getPairAsync } from "@prosopo/contract"; import { datasetWithSolutionHashes } from "@prosopo/datasets"; -import type { Captcha } from "@prosopo/types"; +import { AdminApiPaths, type Captcha } from "@prosopo/types"; import { checkboxClass } from "../support/commands.js"; describe("Captchas", () => { + before(async () => { + const timestamp = new Date().getTime(); + const pair = await getPairAsync(Cypress.env("PROSOPO_PROVIDER_MNEMONIC")); + const signature = u8aToHex(pair.sign(timestamp.toString())); + const adminSiteKeyURL = `http://localhost:9229${AdminApiPaths.SiteKeyRegister}`; + await fetch(adminSiteKeyURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + signature: signature, + timestamp: timestamp.toString(), + }, + body: JSON.stringify({ + siteKey: Cypress.env("PROSOPO_SITE_KEY"), + }), + }); + }); + beforeEach(() => { const solutions = datasetWithSolutionHashes.captchas.map((captcha) => ({ captchaContentId: captcha.captchaContentId, diff --git a/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts b/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts index 2ed5ad3cb3..e40b6335dd 100644 --- a/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts +++ b/demos/cypress-shared/cypress/e2e/correct.captcha.signup.cy.ts @@ -13,12 +13,32 @@ // limitations under the License. /// import "@cypress/xpath"; +import { u8aToHex } from "@polkadot/util"; import { ProsopoDatasetError } from "@prosopo/common"; +import { getPairAsync } from "@prosopo/contract"; import { datasetWithSolutionHashes } from "@prosopo/datasets"; -import type { Captcha } from "@prosopo/types"; +import { AdminApiPaths, type Captcha } from "@prosopo/types"; import { checkboxClass } from "../support/commands.js"; describe("Captchas", () => { + before(async () => { + const timestamp = new Date().getTime(); + const pair = await getPairAsync(Cypress.env("PROSOPO_PROVIDER_MNEMONIC")); + const signature = u8aToHex(pair.sign(timestamp.toString())); + const adminSiteKeyURL = `http://localhost:9229${AdminApiPaths.SiteKeyRegister}`; + await fetch(adminSiteKeyURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + signature: signature, + timestamp: timestamp.toString(), + }, + body: JSON.stringify({ + siteKey: Cypress.env("PROSOPO_SITE_KEY"), + }), + }); + }); + beforeEach(() => { const solutions = datasetWithSolutionHashes.captchas.map((captcha) => ({ captchaContentId: captcha.captchaContentId, diff --git a/dev/config/src/vite/vite.frontend.config.ts b/dev/config/src/vite/vite.frontend.config.ts index d17a3785ea..5466a7db02 100644 --- a/dev/config/src/vite/vite.frontend.config.ts +++ b/dev/config/src/vite/vite.frontend.config.ts @@ -68,6 +68,9 @@ export default async function ( "process.env.PROSOPO_PACKAGE_VERSION": JSON.stringify( process.env.PROSOPO_PACKAGE_VERSION, ), + "process.env.PROSOPO_DOCS_URL": JSON.stringify( + process.env.PROSOPO_DOCS_URL, + ), // only needed if bundling with a site key "process.env.PROSOPO_SITE_KEY": JSON.stringify( process.env.PROSOPO_SITE_KEY, diff --git a/package.json b/package.json index d5f66a7110..dd29168470 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "setup": "npm -w @prosopo/scripts run setup", "setup:all": "npm run setup", "start:provider": "npm -w @prosopo/cli run start", + "start:provider:admin": "npm -w @prosopo/cli run start:admin", "start:server": "npm -w @prosopo/client-example-server run start", "start:demo": "npm -w @prosopo/client-example run start", "start:bundle": "npm -w @prosopo/client-bundle-example run start", diff --git a/packages/api/src/api/ProviderApi.ts b/packages/api/src/api/ProviderApi.ts index 91179c46b2..874336ffe0 100644 --- a/packages/api/src/api/ProviderApi.ts +++ b/packages/api/src/api/ProviderApi.ts @@ -25,6 +25,7 @@ import { type PowCaptchaSolutionResponse, type ProcaptchaToken, type Provider, + type ProviderApiInterface, type ProviderRegistered, type RandomProvider, type ServerPowCaptchaVerifyRequestBodyType, @@ -36,7 +37,10 @@ import { } from "@prosopo/types"; import HttpClientBase from "./HttpClientBase.js"; -export default class ProviderApi extends HttpClientBase implements ProviderApi { +export default class ProviderApi + extends HttpClientBase + implements ProviderApiInterface +{ private account: string; constructor(providerUrl: string, account: string) { diff --git a/packages/cli/package.json b/packages/cli/package.json index 7bb55c004e..0a5cd3216a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -22,6 +22,7 @@ "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts", "cli": "node ./dist/cli.js", "start": "node ./dist/cli.js --api", + "start:admin": "node ./dist/cli.js --api --adminApi", "start:dev": "node ./dist/cli.js --api --dev" }, "dependencies": { diff --git a/packages/cli/src/RateLimiter.ts b/packages/cli/src/RateLimiter.ts index 6ccad8fc96..8b2ca1b526 100644 --- a/packages/cli/src/RateLimiter.ts +++ b/packages/cli/src/RateLimiter.ts @@ -55,14 +55,14 @@ export const getRateLimitConfig = () => { windowMs: process.env.PROSOPO_SUBMIT_USER_EVENTS_WINDOW, limit: process.env.PROSOPO_SUBMIT_USER_EVENTS_LIMIT, }, - [AdminApiPaths.BatchCommit]: { - windowMs: process.env.PROSOPO_BATCH_COMMIT_WINDOW, - limit: process.env.PROSOPO_BATCH_COMMIT_LIMIT, - }, [AdminApiPaths.UpdateDataset]: { windowMs: process.env.PROSOPO_UPDATE_DATASET_WINDOW, limit: process.env.PROSOPO_UPDATE_DATASET_LIMIT, }, + [AdminApiPaths.SiteKeyRegister]: { + windowMs: process.env.PROSOPO_SITE_KEY_REGISTER_WINDOW, + limit: process.env.PROSOPO_SITE_KEY_REGISTER_LIMIT, + }, [AdminApiPaths.ProviderDeregister]: { windowMs: process.env.PROSOPO_PROVIDER_DEREGISTER_WINDOW, limit: process.env.PROSOPO_PROVIDER_DEREGISTER_LIMIT, diff --git a/packages/cli/src/argv.ts b/packages/cli/src/argv.ts index 15fadc6c6c..c81286a754 100644 --- a/packages/cli/src/argv.ts +++ b/packages/cli/src/argv.ts @@ -18,6 +18,7 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { commandProviderSetDataset, + commandSiteKeyRegister, commandStoreCaptchasExternally, commandVersion, } from "./commands/index.js"; @@ -45,6 +46,7 @@ export function processArgs( } as const) .command(commandProviderSetDataset(pair, config, { logger })) .command(commandStoreCaptchasExternally(pair, config, { logger })) + .command(commandSiteKeyRegister(pair, config, { logger })) .command(commandVersion(pair, config, { logger })) .parse(); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 46b5439001..03768441ba 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -14,3 +14,4 @@ export { default as commandProviderSetDataset } from "./providerSetDataset.js"; export { default as commandStoreCaptchasExternally } from "./storeCaptchasExternally.js"; export { default as commandVersion } from "./version.js"; +export { default as commandSiteKeyRegister } from "./siteKeyRegister.js"; diff --git a/packages/cli/src/commands/siteKeyRegister.ts b/packages/cli/src/commands/siteKeyRegister.ts new file mode 100644 index 0000000000..f2e8f5448e --- /dev/null +++ b/packages/cli/src/commands/siteKeyRegister.ts @@ -0,0 +1,53 @@ +// Copyright 2021-2024 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import type { KeyringPair } from "@polkadot/keyring/types"; +import { LogLevel, type Logger, getLogger } from "@prosopo/common"; +import { ProviderEnvironment } from "@prosopo/env"; +import { Tasks } from "@prosopo/provider"; +import type { ProsopoConfigOutput } from "@prosopo/types"; +import type { ArgumentsCamelCase, Argv } from "yargs"; +import { validateSiteKey } from "./validators.js"; + +export default ( + pair: KeyringPair, + config: ProsopoConfigOutput, + cmdArgs?: { logger?: Logger }, +) => { + const logger = + cmdArgs?.logger || getLogger(LogLevel.enum.info, "cli.dapp_register"); + + return { + command: "site_key_register ", + describe: "Register a Site Key", + builder: (yargs: Argv) => + yargs.positional("sitekey", { + type: "string" as const, + demandOption: true, + desc: "The AccountId of the application to register the Site Key with", + } as const), + handler: async (argv: ArgumentsCamelCase) => { + try { + const env = new ProviderEnvironment(config, pair); + await env.isReady(); + const siteKey = argv.sitekey; + const tasks = new Tasks(env); + await tasks.clientTaskManager.registerSiteKey(siteKey as string); + logger.info(`Site Key ${argv.sitekey} registered`); + } catch (err) { + logger.error(err); + } + }, + middlewares: [validateSiteKey], + }; +}; diff --git a/packages/cli/src/commands/validators.ts b/packages/cli/src/commands/validators.ts index 0440a6a31c..391634cc45 100644 --- a/packages/cli/src/commands/validators.ts +++ b/packages/cli/src/commands/validators.ts @@ -28,6 +28,14 @@ export const validateAddress = ( return { address }; }; +export const validateSiteKey = ( + argv: ArgumentsCamelCase, +): { sitekey: string } => { + const sitekey = encodeStringAddress(argv.sitekey as string); + + return { sitekey }; +}; + export const validateValue = (argv: ArgumentsCamelCase) => { if (typeof argv.value !== "number") { throw new ProsopoEnvError("CLI.PARAMETER_ERROR", { diff --git a/packages/common/src/locales/en.json b/packages/common/src/locales/en.json new file mode 100644 index 0000000000..10af8c87a0 --- /dev/null +++ b/packages/common/src/locales/en.json @@ -0,0 +1,407 @@ +{ + "ACCOUNT": { + "NO_POLKADOT_EXTENSION": "Polkadot extension not found" + }, + "WIDGET": { + "SELECT_ALL": "Select all containing the following", + "IF_NONE_CLICK_NEXT": "If there are none, click Next", + "NEXT": "Next", + "SUBMIT": "Submit", + "CANCEL": "Cancel", + "I_AM_HUMAN": "I am human", + "NO_ACCOUNTS_FOUND": "No accounts found", + "ACCOUNT_NOT_FOUND": "Account not found", + "NO_EXTENSION_FOUND": "No extension found" + }, + "GENERAL": { + "JSON_LOAD_FAILED": "Failed to load JSON file", + "MNEMONIC_UNDEFINED": "Mnemonic Undefined. Please set the mnemonic in environment variables", + "NO_MNEMONIC_OR_SEED": "No mnemonic or seed provided", + "CANT_FIND_KEYRINGPAIR": "Can't find the keyringpair for {{address}}", + "ENVIRONMENT_NOT_READY": "Environment not ready", + "INVALID_SIGNATURE": "Invalid signature", + "NOT_IMPLEMENTED": "Not implemented", + "SITE_KEY_MISSING": "SITE KEY missing", + "ACCOUNT_NOT_FOUND": "Account not found" + }, + "CONTRACT": { + "INVALID_METHOD": "Invalid contract method", + "TX_ERROR": "Error making tx", + "INVALID_ADDRESS": "Failed to encode invalid address", + "INVALID_STORAGE_NAME": "Failed to find given storage name", + "CAPTCHA_SOLUTION_COMMITMENT_DOES_NOT_EXIST": "Captcha solution commitment does not exist", + "CONTRACT_UNDEFINED": "Contract undefined", + "SIGNER_UNDEFINED": "Signer undefined", + "CANNOT_FIND_KEYPAIR": "Cannot find keypair", + "INTERRUPTED_EVENT": "Event interrupted", + "TOO_MANY_CALLS": "Too many calls", + "UNKNOWN_ERROR": "Unknown error", + "CHAIN_DECIMALS_UNDEFINED": "Chain decimals are not defined", + "INVALID_DATA_FORMAT": "Invalid data format", + "TX_QUEUE_ERROR": "Error in Transaction Queue", + "DISPATCH_ERROR": "Error dispatching transaction" + }, + "CONFIG": { + "UNKNOWN_ENVIRONMENT": "Unknown environment requested", + "INVALID_CAPTCHA_NUMBER": "Please configure captchas configurations correctly", + "INVALID_LOG_LEVEL": "Invalid log level", + "INVALID_PACKAGE_DIR": "Invalid package directory" + }, + "DATASET": { + "DATASET_PARSE_ERROR": "Error parsing dataset", + "SOLUTION_PARSE_ERROR": "Error parsing dataset", + "HASH_ERROR": "Error hashing dataset", + "DATASET_ID_UNDEFINED": "Dataset id undefined", + "NOT_ENOUGH_LABELS": "Not enough labels", + "NOT_ENOUGH_IMAGES": "Not enough images", + "CAPTCHAS_COUNT_LESS_THAN_CONFIGURED": "Number of captchas in dataset is less than configured number of captchas", + "SOLUTIONS_COUNT_LESS_THAN_CONFIGURED": "Number of solutions in dataset is less than configured number of solutions", + "DUPLICATE_IMAGE": "duplicate image detected", + "MERKLE_ERROR": "Error creating merkle tree" + }, + "DATABASE": { + "DATABASE_IMPORT_FAILED": "Failed to import database engine", + "DATABASE_UNDEFINED": "Database client is not connected", + "DATABASE_HOST_UNDEFINED": "Database host address is not defined", + "DATASET_LOAD_FAILED": "Data set load failed", + "DATASET_GET_FAILED": "Failed to get dataset", + "CAPTCHA_GET_FAILED": "Failed to get captcha", + "CAPTCHA_UPDATE_FAILED": "Failed to update captcha", + "IMAGE_GET_FAILED": "Failed to get image", + "PENDING_RECORD_NOT_FOUND": "No pending record found", + "INVALID_HASH": "Invalid hash", + "SOLUTION_GET_FAILED": "Failed to get solution", + "DATASET_WITH_SOLUTIONS_GET_FAILED": "No datasets found with required number of solutions", + "SOLUTION_APPROVE_FAILED": "Failed to approve solution", + "SOLUTION_FLAG_FAILED": "Failed to flag solution as processed", + "TABLES_UNDEFINED": "Tables undefined", + "CONNECTION_UNDEFINED": "Connection undefined", + "COMMITMENT_FLAG_FAILED": "Failed to flag commitment as processed" + }, + "CAPTCHA": { + "PARSE_ERROR": "Error parsing captcha", + "INVALID_CAPTCHA_ID": "Invalid captcha id", + "INVALID_ITEM_FORMAT": "Only image and text item types allowed", + "INVALID_SOLUTION_TYPE": "Invalid solution type", + "INVALID_ITEM_HASH": "Invalid item hash", + "DIFFERENT_DATASET_IDS": "Dataset ids do not match", + "INVALID_TIMESTAMP": "Invalid timestamp", + "ID_MISMATCH": "captcha id mismatch", + "MISSING_ITEM_HASH": "missing item hash", + "INVALID_CAPTCHA_CHALLENGE": "Invalid captcha challenge", + "DAPP_USER_SOLUTION_NOT_FOUND": "Dapp user solution not found", + "NO_CAPTCHA": "No captcha found", + "INVALID_TOKEN": "Invalid token", + "INVALID_SOLUTION": "Invalid solution" + }, + "API": { + "CAPTCHA_FAILED": "You answered one or more captchas incorrectly. Please try again", + "CAPTCHA_PASSED": "You correctly answered the captchas", + "BAD_REQUEST": "BadRequest", + "USER_VERIFIED": "User verified", + "USER_NOT_VERIFIED": "User not verified", + "USER_NOT_VERIFIED_TIME_EXPIRED": "User not verified. Captcha solution has expired.", + "USER_NOT_VERIFIED_NO_SOLUTION": "User not verified. No captcha solution found.", + "USER_ALREADY_VERIFIED": "This solution has already been verified. User should complete a new captcha.", + "UNKNOWN": "Unknown API error" + }, + "CLI": { + "PARAMETER_ERROR": "Invalid parameter" + }, + "DEVELOPER": { + "PROSOPO_SITE_KEY_MISSING": "PROSOPO_SITE_KEY is not set in .env file.", + "PROVIDER_NO_CAPTCHA": "No captchas returned from provider", + "MISSING_PROVIDER_PAIR": "Missing provider pair", + "MISSING_ENV_VARIABLE": "Missing environment variable", + "GENERAL": "General Dev Error, see context", + "MISSING_SECRET_KEY": "Missing secret key", + "KEY_ERROR": "Key error", + "METHOD_NOT_IMPLEMENTED": "Method not implemented" + }, + "FS": { + "FILE_NOT_FOUND": "File not found", + "FILE_ALREADY_EXISTS": "File already exists", + "INVALID_DIR_FORMAT": "Invalid directory format" + }, + "bird": "bird", + "bus": "bus", + "car": "car", + "cat": "cat", + "deer": "deer", + "dog": "dog", + "horse": "horse", + "plane": "plane", + "train": "train", + "animals": "animals", + "antelope": "antelope", + "backpack": "backpack", + "badger": "badger", + "baseball-bat": "baseball-bat", + "baseball-glove": "baseball-glove", + "basketball-hoop": "basketball-hoop", + "bat": "bat", + "bathtub": "bathtub", + "bear": "bear", + "bee": "bee", + "beer-mug": "beer-mug", + "beetle": "beetle", + "billiards": "billiards", + "binoculars": "binoculars", + "birdbath": "birdbath", + "bison": "bison", + "blimp": "blimp", + "boar": "boar", + "bonsai-tree": "bonsai-tree", + "boom-box": "boom-box", + "bowling-ball": "bowling-ball", + "bowling-pin": "bowling-pin", + "boxing-glove": "boxing-glove", + "breadmaker": "breadmaker", + "bulldozer": "bulldozer", + "butterfly": "butterfly", + "cactus": "cactus", + "cake": "cake", + "calculator": "calculator", + "camel": "camel", + "canoe": "canoe", + "car-tire": "car-tire", + "caterpillar": "caterpillar", + "cd": "cd", + "cereal-box": "cereal-box", + "chandelier": "chandelier", + "chess-board": "chess-board", + "chimpanzee": "chimpanzee", + "chopsticks": "chopsticks", + "cockroach": "cockroach", + "coffin": "coffin", + "coin": "coin", + "comet": "comet", + "computer-keyboard": "computer-keyboard", + "computer-monitor": "computer-monitor", + "computer-mouse": "computer-mouse", + "conch": "conch", + "cormorant": "cormorant", + "covered-wagon": "covered-wagon", + "cow": "cow", + "cowboy-hat": "cowboy-hat", + "coyote": "coyote", + "crab": "crab", + "crow": "crow", + "desk-globe": "desk-globe", + "diamond-ring": "diamond-ring", + "dice": "dice", + "dolphin": "dolphin", + "donkey": "donkey", + "doorknob": "doorknob", + "dragonfly": "dragonfly", + "drinking-straw": "drinking-straw", + "duck": "duck", + "dumb-bell": "dumb-bell", + "eagle": "eagle", + "eiffel-tower": "eiffel-tower", + "electric-guitar": "electric-guitar", + "elephant": "elephant", + "elk": "elk", + "eyeglasses": "eyeglasses", + "fern": "fern", + "fire-extinguisher": "fire-extinguisher", + "fire-hydrant": "fire-hydrant", + "fire-truck": "fire-truck", + "fireworks": "fireworks", + "flamingo": "flamingo", + "fly": "fly", + "fox": "fox", + "french-horn": "french-horn", + "fried-egg": "fried-egg", + "frisbee": "frisbee", + "frog": "frog", + "frying-pan": "frying-pan", + "galaxy": "galaxy", + "gas-pump": "gas-pump", + "giraffe": "giraffe", + "goat": "goat", + "golden-gate-bridge": "golden-gate-bridge", + "goldfish": "goldfish", + "golf-ball": "golf-ball", + "goose": "goose", + "gorilla": "gorilla", + "grapes": "grapes", + "grasshopper": "grasshopper", + "greyhound": "greyhound", + "guitar-pick": "guitar-pick", + "hamburger": "hamburger", + "hammock": "hammock", + "hamster": "hamster", + "hare": "hare", + "harmonica": "harmonica", + "harp": "harp", + "harpsichord": "harpsichord", + "head-phones": "head-phones", + "hedgehog": "hedgehog", + "helicopter": "helicopter", + "hibiscus": "hibiscus", + "hippopotamus": "hippopotamus", + "homer-simpson": "homer-simpson", + "hornbill": "hornbill", + "horseshoe-crab": "horseshoe-crab", + "hot-air-balloon": "hot-air-balloon", + "hot-dog": "hot-dog", + "hot-tub": "hot-tub", + "hourglass": "hourglass", + "house-fly": "house-fly", + "hummingbird": "hummingbird", + "hyena": "hyena", + "ice-cream-cone": "ice-cream-cone", + "iguana": "iguana", + "ipod": "ipod", + "jellyfish": "jellyfish", + "joy-stick": "joy-stick", + "kangaroo": "kangaroo", + "kayak": "kayak", + "killer-whale": "killer-whale", + "koala": "koala", + "ladder": "ladder", + "ladybugs": "ladybugs", + "laptop": "laptop", + "leopard": "leopard", + "license-plate": "license-plate", + "light-house": "light-house", + "lightbulb": "lightbulb", + "lightning": "lightning", + "lion": "lion", + "lizard": "lizard", + "llama": "llama", + "lobster": "lobster", + "mailbox": "mailbox", + "mandolin": "mandolin", + "mars": "mars", + "mattress": "mattress", + "megaphone": "megaphone", + "microscope": "microscope", + "microwave": "microwave", + "minaret": "minaret", + "minotaur": "minotaur", + "mosquito": "mosquito", + "moth": "moth", + "motorbikes": "motorbikes", + "mountain-bike": "mountain-bike", + "mouse": "mouse", + "mushroom": "mushroom", + "mussels": "mussels", + "necktie": "necktie", + "octopus": "octopus", + "okapi": "okapi", + "orangutan": "orangutan", + "ostrich": "ostrich", + "otter": "otter", + "owl": "owl", + "ox": "ox", + "oyster": "oyster", + "palm-pilot": "palm-pilot", + "palm-tree": "palm-tree", + "panda": "panda", + "paper-shredder": "paper-shredder", + "paperclip": "paperclip", + "parrot": "parrot", + "pci-card": "pci-card", + "pelecaniformes": "pelecaniformes", + "penguin": "penguin", + "photocopier": "photocopier", + "piano": "piano", + "picnic-table": "picnic-table", + "pig": "pig", + "pigeon": "pigeon", + "playing-card": "playing-card", + "porcupine": "porcupine", + "possum": "possum", + "pram": "pram", + "pyramid": "pyramid", + "raccoon": "raccoon", + "radio-telescope": "radio-telescope", + "rainbow": "rainbow", + "rat": "rat", + "refrigerator": "refrigerator", + "reindeer": "reindeer", + "rhinoceros": "rhinoceros", + "rotary-phone": "rotary-phone", + "roulette-wheel": "roulette-wheel", + "saddle": "saddle", + "sandpiper": "sandpiper", + "saturn": "saturn", + "school-bus": "school-bus", + "screwdriver": "screwdriver", + "seahorse": "seahorse", + "seal": "seal", + "segway": "segway", + "self-propelled-lawn-mower": "self-propelled-lawn-mower", + "shark": "shark", + "sheep": "sheep", + "sheet-music": "sheet-music", + "skateboard": "skateboard", + "skunk": "skunk", + "skyscraper": "skyscraper", + "smokestack": "smokestack", + "snail": "snail", + "snake": "snake", + "sneaker": "sneaker", + "snowmobile": "snowmobile", + "soccer-ball": "soccer-ball", + "socks": "socks", + "soda-can": "soda-can", + "spaghetti": "spaghetti", + "sparrow": "sparrow", + "speed-boat": "speed-boat", + "spoon": "spoon", + "squid": "squid", + "squirrel": "squirrel", + "stained-glass": "stained-glass", + "starfish": "starfish", + "steering-wheel": "steering-wheel", + "stirrups": "stirrups", + "sunflower": "sunflower", + "superman": "superman", + "sushi": "sushi", + "swan": "swan", + "t-shirt": "t-shirt", + "teapot": "teapot", + "teddy-bear": "teddy-bear", + "telephone-box": "telephone-box", + "tennis-ball": "tennis-ball", + "tennis-court": "tennis-court", + "tennis-racket": "tennis-racket", + "tiger": "tiger", + "toad": "toad", + "toaster": "toaster", + "tomato": "tomato", + "tombstone": "tombstone", + "top-hat": "top-hat", + "touring-bike": "touring-bike", + "tower-pisa": "tower-pisa", + "traffic-light": "traffic-light", + "treadmill": "treadmill", + "triceratops": "triceratops", + "tricycle": "tricycle", + "tripod": "tripod", + "tuning-fork": "tuning-fork", + "turkey": "turkey", + "turtle": "turtle", + "tweezer": "tweezer", + "umbrella": "umbrella", + "unicorn": "unicorn", + "video-projector": "video-projector", + "washing-machine": "washing-machine", + "watch": "watch", + "waterfall": "waterfall", + "watermelon": "watermelon", + "welding-mask": "welding-mask", + "whale": "whale", + "wheelbarrow": "wheelbarrow", + "windmill": "windmill", + "wine-bottle": "wine-bottle", + "wolf": "wolf", + "wombat": "wombat", + "woodpecker": "woodpecker", + "xylophone": "xylophone", + "yo-yo": "yo-yo", + "zebra": "zebra" +} diff --git a/packages/common/src/locales/es.json b/packages/common/src/locales/es.json new file mode 100644 index 0000000000..a9c3a42c6b --- /dev/null +++ b/packages/common/src/locales/es.json @@ -0,0 +1,398 @@ +{ + "ACCOUNT": { + "NO_POLKADOT_EXTENSION": "Extensión Polkadot no encontrada" + }, + "WIDGET": { + "SELECT_ALL": "Seleccionar todo lo que contiene lo siguiente", + "IF_NONE_CLICK_NEXT": "Si no hay ninguno, haz clic en Siguiente", + "NEXT": "Siguiente", + "SUBMIT": "Enviar", + "CANCEL": "Cancelar", + "I_AM_HUMAN": "Soy humano", + "NO_ACCOUNTS_FOUND": "No se encontraron cuentas", + "ACCOUNT_NOT_FOUND": "Cuenta no encontrada", + "NO_EXTENSION_FOUND": "Extensión no encontrada" + }, + "GENERAL": { + "JSON_LOAD_FAILED": "Error al cargar el archivo JSON", + "MNEMONIC_UNDEFINED": "Mnemónico indefinido. Por favor, establece el mnemónico en las variables de entorno", + "NO_MNEMONIC_OR_SEED": "No se proporcionó mnemónico ni semilla", + "CANT_FIND_KEYRINGPAIR": "No se puede encontrar el keyringpair para {{address}}", + "ENVIRONMENT_NOT_READY": "Entorno no listo", + "INVALID_SIGNATURE": "Firma inválida", + "NOT_IMPLEMENTED": "No implementado", + "SITE_KEY_MISSING": "Falta la CLAVE DEL SITIO", + "ACCOUNT_NOT_FOUND": "Cuenta no encontrada" + }, + "CONTRACT": { + "INVALID_METHOD": "Método de contrato inválido", + "TX_ERROR": "Error al hacer la tx", + "INVALID_ADDRESS": "Error al codificar dirección inválida", + "INVALID_STORAGE_NAME": "No se encontró el nombre de almacenamiento proporcionado", + "CAPTCHA_SOLUTION_COMMITMENT_DOES_NOT_EXIST": "El compromiso de la solución del captcha no existe", + "CONTRACT_UNDEFINED": "Contrato indefinido", + "SIGNER_UNDEFINED": "Firmante indefinido", + "CANNOT_FIND_KEYPAIR": "No se puede encontrar el par de claves", + "INTERRUPTED_EVENT": "Evento interrumpido", + "TOO_MANY_CALLS": "Demasiadas llamadas", + "UNKNOWN_ERROR": "Error desconocido", + "CHAIN_DECIMALS_UNDEFINED": "Decimales de la cadena no definidos", + "INVALID_DATA_FORMAT": "Formato de datos inválido", + "TX_QUEUE_ERROR": "Error en la cola de transacciones", + "DISPATCH_ERROR": "Error al despachar la transacción" + }, + "CONFIG": { + "UNKNOWN_ENVIRONMENT": "Entorno solicitado desconocido", + "INVALID_CAPTCHA_NUMBER": "Por favor configura las captchas correctamente", + "INVALID_LOG_LEVEL": "Nivel de registro inválido", + "INVALID_PACKAGE_DIR": "Directorio del paquete inválido" + }, + "DATASET": { + "DATASET_PARSE_ERROR": "Error al analizar el conjunto de datos", + "SOLUTION_PARSE_ERROR": "Error al analizar el conjunto de soluciones", + "HASH_ERROR": "Error al hashear el conjunto de datos", + "DATASET_ID_UNDEFINED": "ID de conjunto de datos indefinido", + "NOT_ENOUGH_LABELS": "No hay suficientes etiquetas", + "NOT_ENOUGH_IMAGES": "No hay suficientes imágenes", + "CAPTCHAS_COUNT_LESS_THAN_CONFIGURED": "La cantidad de captchas en el conjunto de datos es menor a la cantidad configurada", + "SOLUTIONS_COUNT_LESS_THAN_CONFIGURED": "La cantidad de soluciones en el conjunto de datos es menor a la cantidad configurada", + "DUPLICATE_IMAGE": "Imagen duplicada detectada", + "MERKLE_ERROR": "Error al crear el árbol de Merkle" + }, + "DATABASE": { + "DATABASE_IMPORT_FAILED": "Error al importar el motor de la base de datos", + "DATABASE_UNDEFINED": "El cliente de la base de datos no está conectado", + "DATABASE_HOST_UNDEFINED": "La dirección del host de la base de datos no está definida", + "DATASET_LOAD_FAILED": "Error al cargar el conjunto de datos", + "DATASET_GET_FAILED": "Error al obtener el conjunto de datos", + "CAPTCHA_GET_FAILED": "Error al obtener el captcha", + "CAPTCHA_UPDATE_FAILED": "Error al actualizar el captcha", + "IMAGE_GET_FAILED": "Error al obtener la imagen", + "PENDING_RECORD_NOT_FOUND": "No se encontró ningún registro pendiente", + "INVALID_HASH": "Hash inválido", + "SOLUTION_GET_FAILED": "Error al obtener la solución", + "DATASET_WITH_SOLUTIONS_GET_FAILED": "No se encontraron conjuntos de datos con la cantidad requerida de soluciones", + "SOLUTION_APPROVE_FAILED": "Error al aprobar la solución", + "SOLUTION_FLAG_FAILED": "Error al marcar la solución como procesada", + "TABLES_UNDEFINED": "Tablas indefinidas", + "CONNECTION_UNDEFINED": "Conexión indefinida", + "COMMITMENT_FLAG_FAILED": "Error al marcar el compromiso como procesado" + }, + "CAPTCHA": { + "PARSE_ERROR": "Error al analizar el captcha", + "INVALID_CAPTCHA_ID": "ID de captcha inválido", + "INVALID_ITEM_FORMAT": "Solo se permiten tipos de elementos de imagen y texto", + "INVALID_SOLUTION_TYPE": "Tipo de solución inválido", + "INVALID_ITEM_HASH": "Hash de elemento inválido", + "DIFFERENT_DATASET_IDS": "Los IDs del conjunto de datos no coinciden", + "INVALID_TIMESTAMP": "Marca de tiempo inválida", + "ID_MISMATCH": "Desajuste de ID de captcha", + "MISSING_ITEM_HASH": "Falta el hash del elemento", + "INVALID_CAPTCHA_CHALLENGE": "Desafío de captcha inválido", + "DAPP_USER_SOLUTION_NOT_FOUND": "No se encontró la solución del usuario de la Dapp", + "NO_CAPTCHA": "No se encontró captcha", + "INVALID_TOKEN": "Token inválido", + "INVALID_SOLUTION": "Solución inválida" + }, + "API": { + "CAPTCHA_FAILED": "Respondiste incorrectamente a uno o más captchas. Por favor, inténtalo de nuevo", + "CAPTCHA_PASSED": "Respondiste correctamente a los captchas", + "BAD_REQUEST": "Solicitud incorrecta", + "USER_VERIFIED": "Usuario verificado", + "USER_NOT_VERIFIED": "Usuario no verificado", + "USER_NOT_VERIFIED_TIME_EXPIRED": "Usuario no verificado. La solución del captcha ha expirado.", + "USER_NOT_VERIFIED_NO_SOLUTION": "Usuario no verificado. No se encontró solución de captcha.", + "USER_ALREADY_VERIFIED": "Esta solución ya ha sido verificada. El usuario debe completar un nuevo captcha.", + "UNKNOWN": "Error desconocido en el API" + }, + "CLI": { + "PARAMETER_ERROR": "Parámetro inválido" + }, + "DEVELOPER": { + "PROSOPO_SITE_KEY_MISSING": "PROSOPO_SITE_KEY no está configurado en el archivo .env.", + "PROVIDER_NO_CAPTCHA": "No se devolvieron captchas del proveedor", + "MISSING_PROVIDER_PAIR": "Falta el par de proveedor", + "MISSING_ENV_VARIABLE": "Falta una variable de entorno", + "GENERAL": "Error general de desarrollo, ver contexto", + "MISSING_SECRET_KEY": "Falta la clave secreta", + "KEY_ERROR": "Error de clave", + "METHOD_NOT_IMPLEMENTED": "Método no implementado" + }, + "FS": { + "FILE_NOT_FOUND": "Archivo no encontrado", + "FILE_ALREADY_EXISTS": "El archivo ya existe", + "INVALID_DIR_FORMAT": "Formato de directorio inválido" + }, + "animals": "animales", + "antelope": "antílope", + "backpack": "mochila", + "badger": "tejón", + "baseball-bat": "bate-de-béisbol", + "baseball-glove": "guante-de-béisbol", + "basketball-hoop": "aro-de-baloncesto", + "bat": "murciélago", + "bathtub": "bañera", + "bear": "oso", + "bee": "abeja", + "beer-mug": "jarra-de-cerveza", + "beetle": "escarabajo", + "billiards": "billar", + "binoculars": "binoculares", + "birdbath": "baño-de-aves", + "bison": "bisonte", + "blimp": "dirigible", + "boar": "jabalí", + "bonsai-tree": "árbol-bonsái", + "boom-box": "radiocasete", + "bowling-ball": "bola-de-bolos", + "bowling-pin": "pino-de-bolos", + "boxing-glove": "guante-de-boxeo", + "breadmaker": "panificadora", + "bulldozer": "bulldozer", + "butterfly": "mariposa", + "cactus": "cactus", + "cake": "pastel", + "calculator": "calculadora", + "camel": "camello", + "canoe": "canoa", + "car-tire": "neumático", + "caterpillar": "oruga", + "cd": "cd", + "cereal-box": "caja-de-cereal", + "chandelier": "candelabro", + "chess-board": "tablero-de-ajedrez", + "chimpanzee": "chimpancé", + "chopsticks": "palillos", + "cockroach": "cucaracha", + "coffin": "ataúd", + "coin": "moneda", + "comet": "cometa", + "computer-keyboard": "teclado-de-ordenador", + "computer-monitor": "monitor-de-ordenador", + "computer-mouse": "ratón-de-ordenador", + "conch": "caracola", + "cormorant": "cormorán", + "covered-wagon": "carro-cubierto", + "cow": "vaca", + "cowboy-hat": "sombrero-vaquero", + "coyote": "coyote", + "crab": "cangrejo", + "crow": "cuervo", + "desk-globe": "globo-terráqueo", + "diamond-ring": "anillo-de-diamantes", + "dice": "dados", + "dolphin": "delfín", + "donkey": "burro", + "doorknob": "manilla-de-puerta", + "dragonfly": "libélula", + "drinking-straw": "pajilla", + "duck": "pato", + "dumb-bell": "mancuerna", + "eagle": "águila", + "eiffel-tower": "torre-eiffel", + "electric-guitar": "guitarra-eléctrica", + "elephant": "elefante", + "elk": "alce", + "eyeglasses": "gafas", + "fern": "helecho", + "fire-extinguisher": "extintor", + "fire-hydrant": "hidrante", + "fire-truck": "camión-de-bomberos", + "fireworks": "fuegos-artificiales", + "flamingo": "flamenco", + "fly": "mosca", + "fox": "zorro", + "french-horn": "trompa", + "fried-egg": "huevo-frito", + "frisbee": "frisbee", + "frog": "rana", + "frying-pan": "sartén", + "galaxy": "galaxia", + "gas-pump": "bomba-de-gasolina", + "giraffe": "jirafa", + "goat": "cabra", + "golden-gate-bridge": "puente-golden-gate", + "goldfish": "pez-dorado", + "golf-ball": "pelota-de-golf", + "goose": "ganso", + "gorilla": "gorila", + "grapes": "uvas", + "grasshopper": "saltamontes", + "greyhound": "galgo", + "guitar-pick": "púa-de-guitarra", + "hamburger": "hamburguesa", + "hammock": "hamaca", + "hamster": "hámster", + "hare": "liebre", + "harmonica": "armónica", + "harp": "arpa", + "harpsichord": "clavicémbalo", + "head-phones": "auriculares", + "hedgehog": "erizo", + "helicopter": "helicóptero", + "hibiscus": "hibisco", + "hippopotamus": "hipopótamo", + "homer-simpson": "homer-simpson", + "hornbill": "cálao", + "horseshoe-crab": "cangrejo-herradura", + "hot-air-balloon": "globo-aerostático", + "hot-dog": "perro-caliente", + "hot-tub": "jacuzzi", + "hourglass": "reloj-de-arena", + "house-fly": "mosca-doméstica", + "hummingbird": "colibrí", + "hyena": "hiena", + "ice-cream-cone": "cono-de-helado", + "iguana": "iguana", + "ipod": "ipod", + "jellyfish": "medusa", + "joy-stick": "joystick", + "kangaroo": "canguro", + "kayak": "kayak", + "killer-whale": "orca", + "koala": "koala", + "ladder": "escalera", + "ladybugs": "mariquitas", + "laptop": "portátil", + "leopard": "leopardo", + "license-plate": "placa-de-matrícula", + "light-house": "faro", + "lightbulb": "bombilla", + "lightning": "relámpago", + "lion": "león", + "lizard": "lagarto", + "llama": "llama", + "lobster": "langosta", + "mailbox": "buzón", + "mandolin": "mandolina", + "mars": "marte", + "mattress": "colchón", + "megaphone": "megáfono", + "microscope": "microscopio", + "microwave": "microondas", + "minaret": "minarete", + "minotaur": "minotauro", + "mosquito": "mosquito", + "moth": "polilla", + "motorbikes": "motos", + "mountain-bike": "bicicleta-de-montaña", + "mouse": "ratón", + "mushroom": "seta", + "mussels": "mejillones", + "necktie": "corbata", + "octopus": "pulpo", + "okapi": "okapi", + "orangutan": "orangután", + "ostrich": "avestruz", + "otter": "nutria", + "owl": "búho", + "ox": "buey", + "oyster": "ostra", + "palm-pilot": "palm-pilot", + "palm-tree": "palmera", + "panda": "panda", + "paper-shredder": "trituradora-de-papel", + "paperclip": "clip", + "parrot": "loro", + "pci-card": "tarjeta-pci", + "pelecaniformes": "pelecaniformes", + "penguin": "pingüino", + "photocopier": "fotocopiadora", + "piano": "piano", + "picnic-table": "mesa-de-picnic", + "pig": "cerdo", + "pigeon": "paloma", + "playing-card": "carta", + "porcupine": "puercoespín", + "possum": "zarigüeya", + "pram": "carrito-de-bebé", + "pyramid": "pirámide", + "raccoon": "mapache", + "radio-telescope": "radiotelescopio", + "rainbow": "arcoíris", + "rat": "rata", + "refrigerator": "refrigerador", + "reindeer": "reno", + "rhinoceros": "rinoceronte", + "rotary-phone": "teléfono-rotatorio", + "roulette-wheel": "ruleta", + "saddle": "silla-de-montar", + "sandpiper": "correlimos", + "saturn": "saturno", + "school-bus": "autobús-escolar", + "screwdriver": "destornillador", + "seahorse": "caballito-de-mar", + "seal": "foca", + "segway": "segway", + "self-propelled-lawn-mower": "cortacésped-autopropulsado", + "shark": "tiburón", + "sheep": "oveja", + "sheet-music": "partitura", + "skateboard": "patineta", + "skunk": "mofeta", + "skyscraper": "rascacielos", + "smokestack": "chimenea-industrial", + "snail": "caracol", + "snake": "serpiente", + "sneaker": "zapatilla-deportiva", + "snowmobile": "moto-de-nieve", + "soccer-ball": "balón-de-fútbol", + "socks": "calcetines", + "soda-can": "lata-de-refresco", + "spaghetti": "espagueti", + "sparrow": "gorrión", + "speed-boat": "lancha-rápida", + "spoon": "cuchara", + "squid": "calamar", + "squirrel": "ardilla", + "stained-glass": "vidrieras", + "starfish": "estrella-de-mar", + "steering-wheel": "volante", + "stirrups": "estribos", + "sunflower": "girasol", + "superman": "superman", + "sushi": "sushi", + "swan": "cisne", + "t-shirt": "camiseta", + "teapot": "tetera", + "teddy-bear": "oso-de-peluche", + "telephone-box": "cabina-telefónica", + "tennis-ball": "pelota-de-tenis", + "tennis-court": "cancha-de-tenis", + "tennis-racket": "raqueta-de-tenis", + "tiger": "tigre", + "toad": "sapo", + "toaster": "tostadora", + "tomato": "tomate", + "tombstone": "lápida", + "top-hat": "sombrero-de-copa", + "touring-bike": "bicicleta-de-turismo", + "tower-pisa": "torre-de-pisa", + "traffic-light": "semáforo", + "treadmill": "cinta-de-correr", + "triceratops": "triceratops", + "tricycle": "triciclo", + "tripod": "trípode", + "tuning-fork": "diapasón", + "turkey": "pavo", + "turtle": "tortuga", + "tweezer": "pinzas", + "umbrella": "paraguas", + "unicorn": "unicornio", + "video-projector": "proyector-de-video", + "washing-machine": "lavadora", + "watch": "reloj", + "waterfall": "cascada", + "watermelon": "sandía", + "welding-mask": "máscara-de-soldar", + "whale": "ballena", + "wheelbarrow": "carretilla", + "windmill": "molino-de-viento", + "wine-bottle": "botella-de-vino", + "wolf": "lobo", + "wombat": "wombat", + "woodpecker": "pájaro-carpintero", + "xylophone": "xilófono", + "yo-yo": "yo-yo", + "zebra": "cebra" +} diff --git a/packages/common/src/locales/pt-BR.json b/packages/common/src/locales/pt-BR.json new file mode 100644 index 0000000000..cdf3f9035d --- /dev/null +++ b/packages/common/src/locales/pt-BR.json @@ -0,0 +1,398 @@ +{ + "ACCOUNT": { + "NO_POLKADOT_EXTENSION": "Extensão Polkadot não encontrada" + }, + "WIDGET": { + "SELECT_ALL": "Selecionar todos que contêm o seguinte", + "IF_NONE_CLICK_NEXT": "Se não houver nenhum, clique em Próximo", + "NEXT": "Próximo", + "SUBMIT": "Enviar", + "CANCEL": "Cancelar", + "I_AM_HUMAN": "Eu sou humano", + "NO_ACCOUNTS_FOUND": "Nenhuma conta encontrada", + "ACCOUNT_NOT_FOUND": "Conta não encontrada", + "NO_EXTENSION_FOUND": "Nenhuma extensão encontrada" + }, + "GENERAL": { + "JSON_LOAD_FAILED": "Falha ao carregar o arquivo JSON", + "MNEMONIC_UNDEFINED": "Mnemônico indefinido. Por favor, defina o mnemônico nas variáveis de ambiente", + "NO_MNEMONIC_OR_SEED": "Nenhum mnemônico ou seed fornecido", + "CANT_FIND_KEYRINGPAIR": "Não é possível encontrar o keyringpair para {{address}}", + "ENVIRONMENT_NOT_READY": "Ambiente não pronto", + "INVALID_SIGNATURE": "Assinatura inválida", + "NOT_IMPLEMENTED": "Não implementado", + "SITE_KEY_MISSING": "CHAVE DO SITE ausente", + "ACCOUNT_NOT_FOUND": "Conta não encontrada" + }, + "CONTRACT": { + "INVALID_METHOD": "Método de contrato inválido", + "TX_ERROR": "Erro ao fazer tx", + "INVALID_ADDRESS": "Falha ao codificar endereço inválido", + "INVALID_STORAGE_NAME": "Falha ao encontrar o nome de armazenamento fornecido", + "CAPTCHA_SOLUTION_COMMITMENT_DOES_NOT_EXIST": "O compromisso da solução do captcha não existe", + "CONTRACT_UNDEFINED": "Contrato indefinido", + "SIGNER_UNDEFINED": "Assinante indefinido", + "CANNOT_FIND_KEYPAIR": "Não é possível encontrar o par de chaves", + "INTERRUPTED_EVENT": "Evento interrompido", + "TOO_MANY_CALLS": "Muitas chamadas", + "UNKNOWN_ERROR": "Erro desconhecido", + "CHAIN_DECIMALS_UNDEFINED": "Decimais da cadeia não definidos", + "INVALID_DATA_FORMAT": "Formato de dados inválido", + "TX_QUEUE_ERROR": "Erro na fila de transações", + "DISPATCH_ERROR": "Erro ao despachar a transação" + }, + "CONFIG": { + "UNKNOWN_ENVIRONMENT": "Ambiente solicitado desconhecido", + "INVALID_CAPTCHA_NUMBER": "Por favor, configure corretamente os captchas", + "INVALID_LOG_LEVEL": "Nível de log inválido", + "INVALID_PACKAGE_DIR": "Diretório do pacote inválido" + }, + "DATASET": { + "DATASET_PARSE_ERROR": "Erro ao analisar o conjunto de dados", + "SOLUTION_PARSE_ERROR": "Erro ao analisar o conjunto de soluções", + "HASH_ERROR": "Erro ao gerar hash do conjunto de dados", + "DATASET_ID_UNDEFINED": "ID de conjunto de dados indefinido", + "NOT_ENOUGH_LABELS": "Não há rótulos suficientes", + "NOT_ENOUGH_IMAGES": "Não há imagens suficientes", + "CAPTCHAS_COUNT_LESS_THAN_CONFIGURED": "O número de captchas no conjunto de dados é menor que o número configurado", + "SOLUTIONS_COUNT_LESS_THAN_CONFIGURED": "O número de soluções no conjunto de dados é menor que o número configurado", + "DUPLICATE_IMAGE": "Imagem duplicada detectada", + "MERKLE_ERROR": "Erro ao criar a árvore de Merkle" + }, + "DATABASE": { + "DATABASE_IMPORT_FAILED": "Falha ao importar o mecanismo do banco de dados", + "DATABASE_UNDEFINED": "Cliente do banco de dados não está conectado", + "DATABASE_HOST_UNDEFINED": "Endereço do host do banco de dados não está definido", + "DATASET_LOAD_FAILED": "Falha ao carregar o conjunto de dados", + "DATASET_GET_FAILED": "Falha ao obter o conjunto de dados", + "CAPTCHA_GET_FAILED": "Falha ao obter o captcha", + "CAPTCHA_UPDATE_FAILED": "Falha ao atualizar o captcha", + "IMAGE_GET_FAILED": "Falha ao obter a imagem", + "PENDING_RECORD_NOT_FOUND": "Nenhum registro pendente encontrado", + "INVALID_HASH": "Hash inválido", + "SOLUTION_GET_FAILED": "Falha ao obter a solução", + "DATASET_WITH_SOLUTIONS_GET_FAILED": "Nenhum conjunto de dados encontrado com o número necessário de soluções", + "SOLUTION_APPROVE_FAILED": "Falha ao aprovar a solução", + "SOLUTION_FLAG_FAILED": "Falha ao marcar a solução como processada", + "TABLES_UNDEFINED": "Tabelas indefinidas", + "CONNECTION_UNDEFINED": "Conexão indefinida", + "COMMITMENT_FLAG_FAILED": "Falha ao marcar o compromisso como processado" + }, + "CAPTCHA": { + "PARSE_ERROR": "Erro ao analisar o captcha", + "INVALID_CAPTCHA_ID": "ID de captcha inválido", + "INVALID_ITEM_FORMAT": "Somente itens do tipo imagem e texto são permitidos", + "INVALID_SOLUTION_TYPE": "Tipo de solução inválido", + "INVALID_ITEM_HASH": "Hash do item inválido", + "DIFFERENT_DATASET_IDS": "Os IDs do conjunto de dados não correspondem", + "INVALID_TIMESTAMP": "Carimbo de data/hora inválido", + "ID_MISMATCH": "Divergência no ID do captcha", + "MISSING_ITEM_HASH": "Hash do item ausente", + "INVALID_CAPTCHA_CHALLENGE": "Desafio de captcha inválido", + "DAPP_USER_SOLUTION_NOT_FOUND": "Solução do usuário da Dapp não encontrada", + "NO_CAPTCHA": "Nenhum captcha encontrado", + "INVALID_TOKEN": "Token inválido", + "INVALID_SOLUTION": "Solução inválida" + }, + "API": { + "CAPTCHA_FAILED": "Você respondeu incorretamente a um ou mais captchas. Por favor, tente novamente", + "CAPTCHA_PASSED": "Você respondeu corretamente aos captchas", + "BAD_REQUEST": "Solicitação Inválida", + "USER_VERIFIED": "Usuário verificado", + "USER_NOT_VERIFIED": "Usuário não verificado", + "USER_NOT_VERIFIED_TIME_EXPIRED": "Usuário não verificado. A solução do captcha expirou.", + "USER_NOT_VERIFIED_NO_SOLUTION": "Usuário não verificado. Nenhuma solução de captcha encontrada.", + "USER_ALREADY_VERIFIED": "Esta solução já foi verificada. O usuário deve completar um novo captcha.", + "UNKNOWN": "Erro desconhecido na API" + }, + "CLI": { + "PARAMETER_ERROR": "Parâmetro inválido" + }, + "DEVELOPER": { + "PROSOPO_SITE_KEY_MISSING": "PROSOPO_SITE_KEY não está definido no arquivo .env.", + "PROVIDER_NO_CAPTCHA": "Nenhum captcha retornado do provedor", + "MISSING_PROVIDER_PAIR": "Par de provedor ausente", + "MISSING_ENV_VARIABLE": "Variável de ambiente ausente", + "GENERAL": "Erro geral de desenvolvimento, veja o contexto", + "MISSING_SECRET_KEY": "Chave secreta ausente", + "KEY_ERROR": "Erro de chave", + "METHOD_NOT_IMPLEMENTED": "Método não implementado" + }, + "FS": { + "FILE_NOT_FOUND": "Arquivo não encontrado", + "FILE_ALREADY_EXISTS": "Arquivo já existe", + "INVALID_DIR_FORMAT": "Formato de diretório inválido" + }, + "animals": "animais", + "antelope": "antílope", + "backpack": "mochila", + "badger": "texugo", + "baseball-bat": "taco-de-baseball", + "baseball-glove": "luva-de-baseball", + "basketball-hoop": "cesta-de-basquete", + "bat": "morcego", + "bathtub": "banheira", + "bear": "urso", + "bee": "abelha", + "beer-mug": "caneca-de-cerveja", + "beetle": "besouro", + "billiards": "sinuca", + "binoculars": "binóculos", + "birdbath": "bebedouro-para-pássaros", + "bison": "bisonte-ou-bisão", + "blimp": "dirigível", + "boar": "javali", + "bonsai-tree": "árvore-bonsai", + "boom-box": "aparelho-de-som-portátil", + "bowling-ball": "bola-de-boliche", + "bowling-pin": "pino-de-boliche", + "boxing-glove": "luva-de-boxe", + "breadmaker": "máquina-de-pão", + "bulldozer": "trator", + "butterfly": "borboleta", + "cactus": "cacto", + "cake": "bolo", + "calculator": "calculadora", + "camel": "camelo", + "canoe": "canoa", + "car-tire": "pneu", + "caterpillar": "lagarta", + "cd": "cd", + "cereal-box": "caixa-de-cereal", + "chandelier": "lustre", + "chess-board": "tabuleiro-de-xadrez", + "chimpanzee": "chimpanzé", + "chopsticks": "hashis", + "cockroach": "barata", + "coffin": "caixão", + "coin": "moeda", + "comet": "cometa", + "computer-keyboard": "teclado-de-computador", + "computer-monitor": "monitor-de-computador", + "computer-mouse": "mouse-de-computador", + "conch": "concha", + "cormorant": "corvo-marinho", + "covered-wagon": "carroça-coberta", + "cow": "vaca", + "cowboy-hat": "chapéu-de-cowboy", + "coyote": "coiote", + "crab": "caranguejo", + "crow": "corvo", + "desk-globe": "globo-terrestre", + "diamond-ring": "anel-de-diamante", + "dice": "dados", + "dolphin": "golfinho", + "donkey": "burro", + "doorknob": "maçaneta", + "dragonfly": "libélula", + "drinking-straw": "canudo", + "duck": "pato", + "dumb-bell": "halter", + "eagle": "águia", + "eiffel-tower": "torre-eiffel", + "electric-guitar": "guitarra-elétrica", + "elephant": "elefante", + "elk": "alce", + "eyeglasses": "óculos", + "fern": "samambaia", + "fire-extinguisher": "extintor-de-incêndio", + "fire-hydrant": "hidrante", + "fire-truck": "caminhão-de-bombeiros", + "fireworks": "fogos-de-artifício", + "flamingo": "flamingo", + "fly": "mosca", + "fox": "raposa", + "french-horn": "trompa", + "fried-egg": "ovo-frito", + "frisbee": "frisbee", + "frog": "sapo", + "frying-pan": "frigideira", + "galaxy": "galáxia", + "gas-pump": "bomba-de-gasolina", + "giraffe": "girafa", + "goat": "cabra", + "golden-gate-bridge": "ponte-golden-gate", + "goldfish": "peixinho-dourado", + "golf-ball": "bola-de-golfe", + "goose": "ganso", + "gorilla": "gorila", + "grapes": "uvas", + "grasshopper": "gafanhoto", + "greyhound": "galgo", + "guitar-pick": "palheta", + "hamburger": "hambúrguer", + "hammock": "rede", + "hamster": "hamster", + "hare": "lebre", + "harmonica": "gaita", + "harp": "harpa", + "harpsichord": "cravo", + "head-phones": "fones-de-ouvido", + "hedgehog": "ouriço", + "helicopter": "helicóptero", + "hibiscus": "hibisco", + "hippopotamus": "hipopótamo", + "homer-simpson": "homer-simpson", + "hornbill": "calau", + "horseshoe-crab": "caranguejo-ferradura", + "hot-air-balloon": "balão-de-ar-quente", + "hot-dog": "cachorro-quente", + "hot-tub": "banheira-de-hidromassagem", + "hourglass": "ampulheta", + "house-fly": "mosca-doméstica", + "hummingbird": "beija-flor", + "hyena": "hiena", + "ice-cream-cone": "casquinha-de-sorvete", + "iguana": "iguana", + "ipod": "ipod", + "jellyfish": "água-viva", + "joy-stick": "joystick", + "kangaroo": "canguru", + "kayak": "caiaque", + "killer-whale": "orca", + "koala": "coala", + "ladder": "escada", + "ladybugs": "joaninhas", + "laptop": "laptop", + "leopard": "leopardo", + "license-plate": "placa-de-carro", + "light-house": "farol", + "lightbulb": "lâmpada", + "lightning": "raio", + "lion": "leão", + "lizard": "lagarto", + "llama": "lhama", + "lobster": "lagosta", + "mailbox": "caixa-de-correio", + "mandolin": "bandolim", + "mars": "marte", + "mattress": "colchão", + "megaphone": "megafone", + "microscope": "microscópio", + "microwave": "micro-ondas", + "minaret": "minarete", + "minotaur": "minotauro", + "mosquito": "mosquito", + "moth": "mariposa", + "motorbikes": "motos", + "mountain-bike": "mountain-bik", + "mouse": "camundongo", + "mushroom": "cogumelo", + "mussels": "mexilhões", + "necktie": "gravata", + "octopus": "polvo", + "okapi": "ocapi", + "orangutan": "orangotango", + "ostrich": "avestruz", + "otter": "lontra", + "owl": "coruja", + "ox": "boi", + "oyster": "ostra", + "palm-pilot": "palm-pilot", + "palm-tree": "palmeira", + "panda": "panda", + "paper-shredder": "triturador-de-papel", + "paperclip": "clipe", + "parrot": "papagaio", + "pci-card": "placa-pci", + "pelecaniformes": "pelecaniformes", + "penguin": "pinguim", + "photocopier": "fotocopiadora", + "piano": "piano", + "picnic-table": "mesa-de-piquenique", + "pig": "porco", + "pigeon": "pombo", + "playing-card": "carta-de-baralho", + "porcupine": "porco-espinho", + "possum": "gambá", + "pram": "carrinho-de-bebê", + "pyramid": "pirâmide", + "raccoon": "guaxinim", + "radio-telescope": "radiotelescópio", + "rainbow": "arco-íris", + "rat": "rato", + "refrigerator": "geladeira", + "reindeer": "rena", + "rhinoceros": "rinoceronte", + "rotary-phone": "telefone-rotativo", + "roulette-wheel": "roleta", + "saddle": "sela", + "sandpiper": "maçarico", + "saturn": "saturno", + "school-bus": "ônibus-escolar", + "screwdriver": "chave-de-fenda", + "seahorse": "cavalo-marinho", + "seal": "foca", + "segway": "segway", + "self-propelled-lawn-mower": "cortador-de-grama-autopropelido", + "shark": "tubarão", + "sheep": "ovelha", + "sheet-music": "partitura", + "skateboard": "skate", + "skunk": "gambá", + "skyscraper": "arranha-céu", + "smokestack": "chaminé", + "snail": "caracol", + "snake": "cobra", + "sneaker": "tênis", + "snowmobile": "moto-de-neve", + "soccer-ball": "bola-de-futebol", + "socks": "meias", + "soda-can": "lata-de-refrigerante", + "spaghetti": "espaguete", + "sparrow": "pardal", + "speed-boat": "lancha", + "spoon": "colher", + "squid": "lula", + "squirrel": "esquilo", + "stained-glass": "vitrais", + "starfish": "estrela-do-mar", + "steering-wheel": "volante", + "stirrups": "estribos", + "sunflower": "girassol", + "superman": "superman", + "sushi": "sushi", + "swan": "cisne", + "t-shirt": "camiseta", + "teapot": "bule", + "teddy-bear": "urso-de-pelúcia", + "telephone-box": "cabine-telefônica", + "tennis-ball": "bola-de-tênis", + "tennis-court": "quadra-de-tênis", + "tennis-racket": "raquete-de-tênis", + "tiger": "tigre", + "toad": "sapo", + "toaster": "torradeira", + "tomato": "tomate", + "tombstone": "lápide", + "top-hat": "cartola", + "touring-bike": "bicicleta-de-turismo", + "tower-pisa": "torre-de-pisa", + "traffic-light": "semáforo", + "treadmill": "esteira", + "triceratops": "tricerátopo", + "tricycle": "triciclo", + "tripod": "tripé", + "tuning-fork": "diapasão", + "turkey": "peru", + "turtle": "tartaruga", + "tweezer": "pinça", + "umbrella": "guarda-chuva", + "unicorn": "unicórnio", + "video-projector": "projetor-de-vídeo", + "washing-machine": "máquina-de-lavar", + "watch": "relógio", + "waterfall": "cachoeira", + "watermelon": "melancia", + "welding-mask": "máscara-de-solda", + "whale": "baleia", + "wheelbarrow": "carrinho-de-mão", + "windmill": "moinho-de-vento", + "wine-bottle": "garrafa-de-vinho", + "wolf": "lobo", + "wombat": "vombate", + "woodpecker": "pica-pau", + "xylophone": "xilofone", + "yo-yo": "ioiô", + "zebra": "zebra" +} diff --git a/packages/common/src/locales/pt.json b/packages/common/src/locales/pt.json new file mode 100644 index 0000000000..32e44439fb --- /dev/null +++ b/packages/common/src/locales/pt.json @@ -0,0 +1,398 @@ +{ + "ACCOUNT": { + "NO_POLKADOT_EXTENSION": "Extensão Polkadot não encontrada" + }, + "WIDGET": { + "SELECT_ALL": "Selecionar todos que contêm o seguinte", + "IF_NONE_CLICK_NEXT": "Se não houver nenhum, clique em Próximo", + "NEXT": "Próximo", + "SUBMIT": "Enviar", + "CANCEL": "Cancelar", + "I_AM_HUMAN": "Eu sou humano", + "NO_ACCOUNTS_FOUND": "Nenhuma conta encontrada", + "ACCOUNT_NOT_FOUND": "Conta não encontrada", + "NO_EXTENSION_FOUND": "Nenhuma extensão encontrada" + }, + "GENERAL": { + "JSON_LOAD_FAILED": "Falha ao carregar o arquivo JSON", + "MNEMONIC_UNDEFINED": "Mnemônico indefinido. Por favor, defina o mnemônico nas variáveis de ambiente", + "NO_MNEMONIC_OR_SEED": "Nenhum mnemônico ou seed fornecido", + "CANT_FIND_KEYRINGPAIR": "Não é possível encontrar o keyringpair para {{address}}", + "ENVIRONMENT_NOT_READY": "Ambiente não pronto", + "INVALID_SIGNATURE": "Assinatura inválida", + "NOT_IMPLEMENTED": "Não implementado", + "SITE_KEY_MISSING": "CHAVE DO SITE ausente", + "ACCOUNT_NOT_FOUND": "Conta não encontrada" + }, + "CONTRACT": { + "INVALID_METHOD": "Método de contrato inválido", + "TX_ERROR": "Erro ao fazer tx", + "INVALID_ADDRESS": "Falha ao codificar endereço inválido", + "INVALID_STORAGE_NAME": "Falha ao encontrar o nome de armazenamento fornecido", + "CAPTCHA_SOLUTION_COMMITMENT_DOES_NOT_EXIST": "O compromisso da solução do captcha não existe", + "CONTRACT_UNDEFINED": "Contrato indefinido", + "SIGNER_UNDEFINED": "Assinante indefinido", + "CANNOT_FIND_KEYPAIR": "Não é possível encontrar o par de chaves", + "INTERRUPTED_EVENT": "Evento interrompido", + "TOO_MANY_CALLS": "Muitas chamadas", + "UNKNOWN_ERROR": "Erro desconhecido", + "CHAIN_DECIMALS_UNDEFINED": "Decimais da cadeia não definidos", + "INVALID_DATA_FORMAT": "Formato de dados inválido", + "TX_QUEUE_ERROR": "Erro na fila de transações", + "DISPATCH_ERROR": "Erro ao despachar a transação" + }, + "CONFIG": { + "UNKNOWN_ENVIRONMENT": "Ambiente solicitado desconhecido", + "INVALID_CAPTCHA_NUMBER": "Por favor, configure corretamente os captchas", + "INVALID_LOG_LEVEL": "Nível de log inválido", + "INVALID_PACKAGE_DIR": "Diretório do pacote inválido" + }, + "DATASET": { + "DATASET_PARSE_ERROR": "Erro ao analisar o conjunto de dados", + "SOLUTION_PARSE_ERROR": "Erro ao analisar o conjunto de soluções", + "HASH_ERROR": "Erro ao gerar hash do conjunto de dados", + "DATASET_ID_UNDEFINED": "ID de conjunto de dados indefinido", + "NOT_ENOUGH_LABELS": "Não há rótulos suficientes", + "NOT_ENOUGH_IMAGES": "Não há imagens suficientes", + "CAPTCHAS_COUNT_LESS_THAN_CONFIGURED": "O número de captchas no conjunto de dados é menor que o número configurado", + "SOLUTIONS_COUNT_LESS_THAN_CONFIGURED": "O número de soluções no conjunto de dados é menor que o número configurado", + "DUPLICATE_IMAGE": "Imagem duplicada detectada", + "MERKLE_ERROR": "Erro ao criar a árvore de Merkle" + }, + "DATABASE": { + "DATABASE_IMPORT_FAILED": "Falha ao importar o mecanismo do banco de dados", + "DATABASE_UNDEFINED": "Cliente do banco de dados não está conectado", + "DATABASE_HOST_UNDEFINED": "Endereço do host do banco de dados não está definido", + "DATASET_LOAD_FAILED": "Falha ao carregar o conjunto de dados", + "DATASET_GET_FAILED": "Falha ao obter o conjunto de dados", + "CAPTCHA_GET_FAILED": "Falha ao obter o captcha", + "CAPTCHA_UPDATE_FAILED": "Falha ao atualizar o captcha", + "IMAGE_GET_FAILED": "Falha ao obter a imagem", + "PENDING_RECORD_NOT_FOUND": "Nenhum registro pendente encontrado", + "INVALID_HASH": "Hash inválido", + "SOLUTION_GET_FAILED": "Falha ao obter a solução", + "DATASET_WITH_SOLUTIONS_GET_FAILED": "Nenhum conjunto de dados encontrado com o número necessário de soluções", + "SOLUTION_APPROVE_FAILED": "Falha ao aprovar a solução", + "SOLUTION_FLAG_FAILED": "Falha ao marcar a solução como processada", + "TABLES_UNDEFINED": "Tabelas indefinidas", + "CONNECTION_UNDEFINED": "Conexão indefinida", + "COMMITMENT_FLAG_FAILED": "Falha ao marcar o compromisso como processado" + }, + "CAPTCHA": { + "PARSE_ERROR": "Erro ao analisar o captcha", + "INVALID_CAPTCHA_ID": "ID de captcha inválido", + "INVALID_ITEM_FORMAT": "Somente tipos de itens de imagem e texto são permitidos", + "INVALID_SOLUTION_TYPE": "Tipo de solução inválido", + "INVALID_ITEM_HASH": "Hash do item inválido", + "DIFFERENT_DATASET_IDS": "Os IDs do conjunto de dados não correspondem", + "INVALID_TIMESTAMP": "Carimbo de data/hora inválido", + "ID_MISMATCH": "Desajuste de ID do captcha", + "MISSING_ITEM_HASH": "Hash do item ausente", + "INVALID_CAPTCHA_CHALLENGE": "Desafio de captcha inválido", + "DAPP_USER_SOLUTION_NOT_FOUND": "Solução do usuário da Dapp não encontrada", + "NO_CAPTCHA": "Nenhum captcha encontrado", + "INVALID_TOKEN": "Token inválido", + "INVALID_SOLUTION": "Solução inválida" + }, + "API": { + "CAPTCHA_FAILED": "Você respondeu incorretamente a um ou mais captchas. Por favor, tente novamente", + "CAPTCHA_PASSED": "Você respondeu corretamente aos captchas", + "BAD_REQUEST": "Solicitação Inválida", + "USER_VERIFIED": "Usuário verificado", + "USER_NOT_VERIFIED": "Usuário não verificado", + "USER_NOT_VERIFIED_TIME_EXPIRED": "Usuário não verificado. A solução do captcha expirou.", + "USER_NOT_VERIFIED_NO_SOLUTION": "Usuário não verificado. Nenhuma solução de captcha encontrada.", + "USER_ALREADY_VERIFIED": "Esta solução já foi verificada. O usuário deve completar um novo captcha.", + "UNKNOWN": "Erro desconhecido na API" + }, + "CLI": { + "PARAMETER_ERROR": "Parâmetro inválido" + }, + "DEVELOPER": { + "PROSOPO_SITE_KEY_MISSING": "PROSOPO_SITE_KEY não está definido no arquivo .env.", + "PROVIDER_NO_CAPTCHA": "Nenhum captcha retornado do provedor", + "MISSING_PROVIDER_PAIR": "Par de provedor ausente", + "MISSING_ENV_VARIABLE": "Variável de ambiente ausente", + "GENERAL": "Erro geral de desenvolvimento, veja o contexto", + "MISSING_SECRET_KEY": "Chave secreta ausente", + "KEY_ERROR": "Erro de chave", + "METHOD_NOT_IMPLEMENTED": "Método não implementado" + }, + "FS": { + "FILE_NOT_FOUND": "Arquivo não encontrado", + "FILE_ALREADY_EXISTS": "Arquivo já existe", + "INVALID_DIR_FORMAT": "Formato de diretório inválido" + }, + "animals": "animais", + "antelope": "antílope", + "backpack": "mochila", + "badger": "texugo", + "baseball-bat": "taco-de-baseball", + "baseball-glove": "luva-de-baseball", + "basketball-hoop": "cesta-de-basquete", + "bat": "morcego", + "bathtub": "banheira", + "bear": "urso", + "bee": "abelha", + "beer-mug": "caneca-de-cerveja", + "beetle": "besouro", + "billiards": "sinuca", + "binoculars": "binóculos", + "birdbath": "bebedouro-para-pássaros", + "bison": "bisonte", + "blimp": "dirigível", + "boar": "javali", + "bonsai-tree": "árvore-bonsai", + "boom-box": "rádio-gravador", + "bowling-ball": "bola-de-boliche", + "bowling-pin": "pino-de-boliche", + "boxing-glove": "luva-de-boxe", + "breadmaker": "máquina-de-pão", + "bulldozer": "trator", + "butterfly": "borboleta", + "cactus": "cacto", + "cake": "bolo", + "calculator": "calculadora", + "camel": "camelo", + "canoe": "canoa", + "car-tire": "pneu", + "caterpillar": "lagarta", + "cd": "cd", + "cereal-box": "caixa-de-cereal", + "chandelier": "lustre", + "chess-board": "tabuleiro-de-xadrez", + "chimpanzee": "chimpanzé", + "chopsticks": "hashis", + "cockroach": "barata", + "coffin": "caixão", + "coin": "moeda", + "comet": "cometa", + "computer-keyboard": "teclado-de-computador", + "computer-monitor": "monitor-de-computador", + "computer-mouse": "mouse-de-computador", + "conch": "concha", + "cormorant": "corvo-marinho", + "covered-wagon": "carroça-coberta", + "cow": "vaca", + "cowboy-hat": "chapéu-de-cowboy", + "coyote": "coiote", + "crab": "caranguejo", + "crow": "corvo", + "desk-globe": "globo-terrestre", + "diamond-ring": "anel-de-diamante", + "dice": "dados", + "dolphin": "golfinho", + "donkey": "burro", + "doorknob": "maçaneta", + "dragonfly": "libélula", + "drinking-straw": "canudo", + "duck": "pato", + "dumb-bell": "halter", + "eagle": "águia", + "eiffel-tower": "torre-eiffel", + "electric-guitar": "guitarra-elétrica", + "elephant": "elefante", + "elk": "alce", + "eyeglasses": "óculos", + "fern": "samambaia", + "fire-extinguisher": "extintor-de-incêndio", + "fire-hydrant": "hidrante", + "fire-truck": "caminhão-de-bombeiros", + "fireworks": "fogos-de-artifício", + "flamingo": "flamingo", + "fly": "mosca", + "fox": "raposa", + "french-horn": "trompa", + "fried-egg": "ovo-frito", + "frisbee": "frisbee", + "frog": "sapo", + "frying-pan": "frigideira", + "galaxy": "galáxia", + "gas-pump": "bomba-de-gasolina", + "giraffe": "girafa", + "goat": "cabra", + "golden-gate-bridge": "ponte-golden-gate", + "goldfish": "peixinho-dourado", + "golf-ball": "bola-de-golfe", + "goose": "ganso", + "gorilla": "gorila", + "grapes": "uvas", + "grasshopper": "gafanhoto", + "greyhound": "galgo", + "guitar-pick": "palheta", + "hamburger": "hambúrguer", + "hammock": "rede", + "hamster": "hamster", + "hare": "lebre", + "harmonica": "gaita", + "harp": "harpa", + "harpsichord": "cravo", + "head-phones": "fones-de-ouvido", + "hedgehog": "ouriço", + "helicopter": "helicóptero", + "hibiscus": "hibisco", + "hippopotamus": "hipopótamo", + "homer-simpson": "homer-simpson", + "hornbill": "calau", + "horseshoe-crab": "caranguejo-ferradura", + "hot-air-balloon": "balão-de-ar-quente", + "hot-dog": "cachorro-quente", + "hot-tub": "banheira-de-hidromassagem", + "hourglass": "ampulheta", + "house-fly": "mosca-doméstica", + "hummingbird": "beija-flor", + "hyena": "hiena", + "ice-cream-cone": "casquinha-de-sorvete", + "iguana": "iguana", + "ipod": "ipod", + "jellyfish": "água-viva", + "joy-stick": "joystick", + "kangaroo": "canguru", + "kayak": "caiaque", + "killer-whale": "orca", + "koala": "coala", + "ladder": "escada", + "ladybugs": "joaninhas", + "laptop": "laptop", + "leopard": "leopardo", + "license-plate": "placa-de-carro", + "light-house": "farol", + "lightbulb": "lâmpada", + "lightning": "raio", + "lion": "leão", + "lizard": "lagarto", + "llama": "lhama", + "lobster": "lagosta", + "mailbox": "caixa-de-correio", + "mandolin": "bandolim", + "mars": "marte", + "mattress": "colchão", + "megaphone": "megafone", + "microscope": "microscópio", + "microwave": "micro-ondas", + "minaret": "minarete", + "minotaur": "minotauro", + "mosquito": "mosquito", + "moth": "mariposa", + "motorbikes": "motos", + "mountain-bike": "bicicleta-de-montanha", + "mouse": "camundongo", + "mushroom": "cogumelo", + "mussels": "mexilhões", + "necktie": "gravata", + "octopus": "polvo", + "okapi": "ocapi", + "orangutan": "orangotango", + "ostrich": "avestruz", + "otter": "lontra", + "owl": "coruja", + "ox": "boi", + "oyster": "ostra", + "palm-pilot": "palm-pilot", + "palm-tree": "palmeira", + "panda": "panda", + "paper-shredder": "triturador-de-papel", + "paperclip": "clipe", + "parrot": "papagaio", + "pci-card": "cartão-pci", + "pelecaniformes": "pelecaniformes", + "penguin": "pinguim", + "photocopier": "fotocopiadora", + "piano": "piano", + "picnic-table": "mesa-de-picnic", + "pig": "porco", + "pigeon": "pombo", + "playing-card": "carta-de-baralho", + "porcupine": "porco-espinho", + "possum": "gambá", + "pram": "carrinho-de-bebê", + "pyramid": "pirâmide", + "raccoon": "guaxinim", + "radio-telescope": "radiotelescópio", + "rainbow": "arco-íris", + "rat": "rato", + "refrigerator": "geladeira", + "reindeer": "rena", + "rhinoceros": "rinoceronte", + "rotary-phone": "telefone-rotativo", + "roulette-wheel": "roleta", + "saddle": "sela", + "sandpiper": "maçarico", + "saturn": "saturno", + "school-bus": "ônibus-escolar", + "screwdriver": "chave-de-fenda", + "seahorse": "cavalo-marinho", + "seal": "foca", + "segway": "segway", + "self-propelled-lawn-mower": "cortador-de-grama-autopropelido", + "shark": "tubarão", + "sheep": "ovelha", + "sheet-music": "partitura", + "skateboard": "skate", + "skunk": "gambá", + "skyscraper": "arranha-céu", + "smokestack": "chaminé", + "snail": "caracol", + "snake": "cobra", + "sneaker": "tênis", + "snowmobile": "moto-de-neve", + "soccer-ball": "bola-de-futebol", + "socks": "meias", + "soda-can": "lata-de-refrigerante", + "spaghetti": "espaguete", + "sparrow": "pardal", + "speed-boat": "lancha", + "spoon": "colher", + "squid": "lula", + "squirrel": "esquilo", + "stained-glass": "vitrais", + "starfish": "estrela-do-mar", + "steering-wheel": "volante", + "stirrups": "estribos", + "sunflower": "girassol", + "superman": "superman", + "sushi": "sushi", + "swan": "cisne", + "t-shirt": "camiseta", + "teapot": "bule", + "teddy-bear": "urso-de-pelúcia", + "telephone-box": "cabine-telefônica", + "tennis-ball": "bola-de-tênis", + "tennis-court": "quadra-de-tênis", + "tennis-racket": "raquete-de-tênis", + "tiger": "tigre", + "toad": "sapo", + "toaster": "torradeira", + "tomato": "tomate", + "tombstone": "lápide", + "top-hat": "cartola", + "touring-bike": "bicicleta-de-turismo", + "tower-pisa": "torre-de-pisa", + "traffic-light": "semáforo", + "treadmill": "esteira", + "triceratops": "tricerátopo", + "tricycle": "triciclo", + "tripod": "tripé", + "tuning-fork": "diapasão", + "turkey": "peru", + "turtle": "tartaruga", + "tweezer": "pinça", + "umbrella": "guarda-chuva", + "unicorn": "unicórnio", + "video-projector": "projetor-de-vídeo", + "washing-machine": "máquina-de-lavar", + "watch": "relógio", + "waterfall": "cachoeira", + "watermelon": "melancia", + "welding-mask": "máscara-de-solda", + "whale": "baleia", + "wheelbarrow": "carrinho-de-mão", + "windmill": "moinho-de-vento", + "wine-bottle": "garrafa-de-vinho", + "wolf": "lobo", + "wombat": "vombate", + "woodpecker": "pica-pau", + "xylophone": "xilofone", + "yo-yo": "ioiô", + "zebra": "zebra" +} diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts index 9c864cbf93..43acdcdace 100644 --- a/packages/common/src/logger.ts +++ b/packages/common/src/logger.ts @@ -136,3 +136,9 @@ export class Loggable { this.#logger = logger; } } + +export const logError = (err: unknown, logger: Logger): void => { + logger.error( + typeof err === "object" && err ? ("stack" in err ? err.stack : err) : err, + ); +}; diff --git a/packages/common/src/node/UrlConverter.ts b/packages/common/src/node/UrlConverter.ts new file mode 100644 index 0000000000..500a8cf0ce --- /dev/null +++ b/packages/common/src/node/UrlConverter.ts @@ -0,0 +1,327 @@ +// Copyright 2021-2024 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ProsopoError } from "../index.js"; + +export class UrlConverter { + private readonly symbols = [ + "", // empty string == termination symbol / noop. This may occur at the end of a byte array where the first 2 bits are used in the final symbol and the remaining 6 bits are ignored. But, because of the spacing, the remaining 6 bits appear to be a symbol. These 6 bits will be set to 000000. Thus we map 000000 (decimal number 0) to the empty string here. + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "/", + ".", + "-", + "_", + "%", + ":", + "http://", + "https://", + "www.", + ".com", + ".net", + ".org", + ".co.uk", + ".io", + ] as const; + + private symbolToNumMap: { + [key: string]: number; + }; + private numToSymbolMap: { + [key: number]: string; + }; + private readonly longestSymbolLength: number; + private readonly symbolNBits = 6; + private readonly byteNBits = 8; + + constructor() { + this.symbolToNumMap = this.symbols.reduce( + (obj, symb, i, arr) => { + obj[symb] = i; + return obj; + }, + {} as { + [key: string]: number; + }, + ); + this.numToSymbolMap = this.symbols.reduce( + (obj, symb, i, arr) => { + obj[i] = symb; + return obj; + }, + {} as { + [key: number]: string; + }, + ); + this.longestSymbolLength = this.symbols.reduce((longest, symb) => { + return Math.max(longest, symb.length); + }, 0); + const maxSymbols = 2 ** this.symbolNBits; + if (this.symbols.length > maxSymbols) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { + error: `Cannot encode more than ${maxSymbols} symbols`, + context: + "only built to encode 64 symbols. Need to adjust the encoding and decoding scheme for more symbols", + }, + }); + } + const symbols = this.getSymbols(); + if (symbols.length !== new Set(symbols).size) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { error: "Symbols must be unique" }, + }); + } + } + + public symbolToNum(symb: string): number | undefined { + const num = this.symbolToNumNull(symb); + if (num === undefined) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { error: `Could not find number for symbol '${symb}'` }, + }); + } + return num; + } + + public symbolToNumNull(symb: string): number | undefined { + symb = symb.toLowerCase(); + const num = this.symbolToNumMap[symb]; + return num; + } + + public numToSymbol(num: number): string { + const symb = this.numToSymbolNull(num); + if (symb === undefined) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { error: `Could not find symbol for number ${num}` }, + }); + } + return symb; + } + + public numToSymbolNull(num: number): string | undefined { + const symb = this.numToSymbolMap[num]; + return symb; + } + + public encode(url: string): Uint8Array { + url = url.toLowerCase(); + // a symbol is a string of characters which can be mapped to a number + // symbols must be unique, likewise with their number mapping + // convert url symbols to numbers (like looking up ascii char symbols against their ascii codes) + const nums: number[] = []; + const origUrl = url; + // loop through the url until it is empty + while (url.length > 0) { + let len = Math.min(url.length, this.longestSymbolLength); + let num: number | undefined = undefined; + // char by char trim down the next n chars of the url attempting to find a matching symbols + while (num === undefined && len > 0) { + const str = url.slice(0, len); + num = this.symbolToNumNull(str); + if (num === undefined) { + len--; + } + } + // check if couldn't find matching symbol + if (num === undefined) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { + error: `Could not find symbol at '${url}' of '${origUrl}'`, + }, + }); + } + // record the number of the symbol and slice the symbol from the url + nums.push(num); + url = url.slice(len); + } + // numbers will range between 0-63 because there are max 64 symbols so need 6 bits to represent + // 6 bits can be fit in 1 byte, leaving 2 bits as remainder + // hence 3 bytes can house 4 symbols using the left over remainder bits + // loop through every 4 symbols worth of numbers + const nBits = nums.length * this.symbolNBits; + const nBytes = Math.ceil(nBits / this.byteNBits); + const bytes = new Uint8Array(nBytes); + for (let bitCount = 0; bitCount < nBits; ) { + // we need to shift a different amount of bits to pack into bytes + // e.g. + // [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0] // 3 8-bit bytes + // [0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0] // 4 6-bit numbers corresponding to symbols + // + // symbol 1 uses the first 6 bits of the first byte + // symbol 2 uses the last 2 bits of the first byte and the first 4 bits of the second byte + // symbol 3 uses the last 4 bits of the second byte and the first 2 bits of the third byte + // symbol 4 uses the last 6 bits of the third byte + const numIndex = (bitCount / this.symbolNBits) | 0; + const num = nums[numIndex]; + if (num === undefined) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { + error: `Could not find number at index ${numIndex} of '${nums}'`, + }, + }); + } + const byteIndex = (bitCount / this.byteNBits) | 0; + const usedBitsInByte = bitCount % this.byteNBits; + const unusedBitsInByte = this.byteNBits - usedBitsInByte; + + const shift = this.symbolNBits - unusedBitsInByte; + if (shift < 0) { + // can fit the unused bits of the number into the start of the byte + const usedBitsInSymbol = bitCount % this.symbolNBits; + const unusedBitsInSymbol = this.symbolNBits - usedBitsInSymbol; + // truncate the number by bits used + const max = 2 ** unusedBitsInSymbol; + const remNum = num % max; + // make num occupy left most bits of a byte + const shift = this.byteNBits - unusedBitsInSymbol; + const shiftedNum = remNum << shift; + // pack X bits from the symbol into the unused bits of the byte + // biome-ignore lint/style/noNonNullAssertion: ts thinks array is potentially undefined + bytes[byteIndex]! |= shiftedNum; + // filled the unused bits in the byte + bitCount += unusedBitsInSymbol; + } else { + // can only fit part of the start of the number into the remainder of the byte + // biome-ignore lint/style/noNonNullAssertion: ts thinks array is potentially undefined + bytes[byteIndex]! |= num >> shift; + bitCount += unusedBitsInByte; + } + } + + return bytes; + } + + public decode(bytes: Uint8Array): string { + const arr: string[] = []; + // loop through every 6 bits + // any remainder bits are ignored (e.g. 4 bytes == 32 bits, only first 30 bits are used to represent 5 6-bit symbols) + const nBits = bytes.length * this.byteNBits; + let num = 0; + let nBitsNum = 0; + for (let bitCount = 0; bitCount < nBits; ) { + const byteIndex = (bitCount / this.byteNBits) | 0; + const byte = bytes[byteIndex]; + if (byte === undefined) { + throw new ProsopoError("DEVELOPER.GENERAL", { + context: { + error: `Could not find byte at index ${byteIndex} of '${bytes}'`, + }, + }); + } + const usedBitsInByte = bitCount % this.byteNBits; + const unusedBitsInByte = this.byteNBits - usedBitsInByte; + // unused bits correspond to the current symbol, so consume up to 6 bits + const unusedBitsInSymbol = this.symbolNBits - nBitsNum; + const consumeNBits = Math.min(unusedBitsInByte, unusedBitsInSymbol); + // consume the bits from the byte + const slice = this.bitSlice(byte, usedBitsInByte, consumeNBits); + // add to num + num = (num << consumeNBits) | slice; + nBitsNum += consumeNBits; + if (nBitsNum >= this.symbolNBits) { + // collected enough bits for a symbol + const symbol = this.numToSymbol(num); + arr.push(symbol); + // reset num to empty + num = 0; + nBitsNum = 0; + } + bitCount += consumeNBits; + } + return arr.join(""); + } + + private bitSlice(num: number, startBit: number, lenBit: number) { + const truncedLeft = this.bitTruncLeft(num, startBit); + const truncedLen = this.bitTruncRight( + truncedLeft, + Math.max(0, this.byteNBits - lenBit - startBit), + ); + return truncedLen; + } + + private bitTruncRight(num: number, nBits: number) { + return num >> nBits; + } + + private bitTruncLeft(num: number, nBits: number) { + const threshNBits = this.byteNBits - nBits; + const thresh = 2 ** threshNBits; + return thresh <= 0 ? 0 : num % thresh; + } + + public getSymbols(): readonly string[] { + return this.symbols; + } +} + +export default UrlConverter; + +// const converter = new UrlConverter() +// // const url = '9p1:h.comxwww.r/6dhttp:///https://0fe9rywww.s0' +// // const outBytes = new Uint8Array([37,144,105,70,216,108,110,65,141,170,74,192,60,226,91,138,199,0]) +// // console.log(url) +// // const bytes = converter.encode(url) +// // const str = converter.decode(bytes) +// // console.log(str) + +// for(let i = 0; i < 10; i++) { +// const len = Math.random() * 100 + 1 // url lengths between 1-100 +// const symbols = converter.getSymbols() +// const url = Array.from({length: len}, () => symbols[Math.round(Math.random() * symbols.length)]).join('') // random url chars +// const bytes = converter.encode(url) +// const decodedUrl = converter.decode(bytes) +// if(url !== decodedUrl) { +// console.log(bytes) +// console.log(url) +// console.log(decodedUrl) +// converter.decode(bytes) +// } +// } diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts index 7797c784fb..d903d5bf39 100644 --- a/packages/database/src/databases/provider.ts +++ b/packages/database/src/databases/provider.ts @@ -39,7 +39,7 @@ import { type ClientRecord, ClientRecordSchema, DatasetRecordSchema, - type IDatabase, + type IProviderDatabase, type IUserDataSlim, PendingRecordSchema, type PoWCaptchaRecord, @@ -126,7 +126,10 @@ const PROVIDER_TABLES = [ }, ]; -export class ProviderDatabase extends MongoDatabase implements IDatabase { +export class ProviderDatabase + extends MongoDatabase + implements IProviderDatabase +{ tables = {} as Tables; constructor( @@ -1256,4 +1259,14 @@ export class ProviderDatabase extends MongoDatabase implements IDatabase { }); await this.tables?.client.bulkWrite(ops); } + + /** + * @description Get a client record + */ + async getClientRecord(account: string): Promise { + const doc = await this.tables?.client + .findOne({ account }) + .lean(); + return doc ? doc : undefined; + } } diff --git a/packages/locale/src/locales/en.json b/packages/locale/src/locales/en.json index 10af8c87a0..4172e844b0 100644 --- a/packages/locale/src/locales/en.json +++ b/packages/locale/src/locales/en.json @@ -22,7 +22,8 @@ "INVALID_SIGNATURE": "Invalid signature", "NOT_IMPLEMENTED": "Not implemented", "SITE_KEY_MISSING": "SITE KEY missing", - "ACCOUNT_NOT_FOUND": "Account not found" + "ACCOUNT_NOT_FOUND": "Account not found", + "INVALID_TIMESTAMP": "Invalid timestamp" }, "CONTRACT": { "INVALID_METHOD": "Invalid contract method", @@ -103,7 +104,8 @@ "USER_NOT_VERIFIED_TIME_EXPIRED": "User not verified. Captcha solution has expired.", "USER_NOT_VERIFIED_NO_SOLUTION": "User not verified. No captcha solution found.", "USER_ALREADY_VERIFIED": "This solution has already been verified. User should complete a new captcha.", - "UNKNOWN": "Unknown API error" + "UNKNOWN": "Unknown API error", + "SITE_KEY_NOT_REGISTERED": "Site key not registered" }, "CLI": { "PARAMETER_ERROR": "Invalid parameter" diff --git a/packages/locale/src/locales/es.json b/packages/locale/src/locales/es.json index a9c3a42c6b..a1c8f72a9a 100644 --- a/packages/locale/src/locales/es.json +++ b/packages/locale/src/locales/es.json @@ -22,7 +22,8 @@ "INVALID_SIGNATURE": "Firma inválida", "NOT_IMPLEMENTED": "No implementado", "SITE_KEY_MISSING": "Falta la CLAVE DEL SITIO", - "ACCOUNT_NOT_FOUND": "Cuenta no encontrada" + "ACCOUNT_NOT_FOUND": "Cuenta no encontrada", + "INVALID_TIMESTAMP": "Marca de tiempo inválida" }, "CONTRACT": { "INVALID_METHOD": "Método de contrato inválido", @@ -85,7 +86,6 @@ "INVALID_SOLUTION_TYPE": "Tipo de solución inválido", "INVALID_ITEM_HASH": "Hash de elemento inválido", "DIFFERENT_DATASET_IDS": "Los IDs del conjunto de datos no coinciden", - "INVALID_TIMESTAMP": "Marca de tiempo inválida", "ID_MISMATCH": "Desajuste de ID de captcha", "MISSING_ITEM_HASH": "Falta el hash del elemento", "INVALID_CAPTCHA_CHALLENGE": "Desafío de captcha inválido", diff --git a/packages/locale/src/locales/pt-BR.json b/packages/locale/src/locales/pt-BR.json index cdf3f9035d..480d26a217 100644 --- a/packages/locale/src/locales/pt-BR.json +++ b/packages/locale/src/locales/pt-BR.json @@ -22,7 +22,8 @@ "INVALID_SIGNATURE": "Assinatura inválida", "NOT_IMPLEMENTED": "Não implementado", "SITE_KEY_MISSING": "CHAVE DO SITE ausente", - "ACCOUNT_NOT_FOUND": "Conta não encontrada" + "ACCOUNT_NOT_FOUND": "Conta não encontrada", + "INVALID_TIMESTAMP": "Carimbo de data/hora inválido" }, "CONTRACT": { "INVALID_METHOD": "Método de contrato inválido", @@ -85,7 +86,6 @@ "INVALID_SOLUTION_TYPE": "Tipo de solução inválido", "INVALID_ITEM_HASH": "Hash do item inválido", "DIFFERENT_DATASET_IDS": "Os IDs do conjunto de dados não correspondem", - "INVALID_TIMESTAMP": "Carimbo de data/hora inválido", "ID_MISMATCH": "Divergência no ID do captcha", "MISSING_ITEM_HASH": "Hash do item ausente", "INVALID_CAPTCHA_CHALLENGE": "Desafio de captcha inválido", diff --git a/packages/locale/src/locales/pt.json b/packages/locale/src/locales/pt.json index 32e44439fb..4e13474d3b 100644 --- a/packages/locale/src/locales/pt.json +++ b/packages/locale/src/locales/pt.json @@ -22,7 +22,8 @@ "INVALID_SIGNATURE": "Assinatura inválida", "NOT_IMPLEMENTED": "Não implementado", "SITE_KEY_MISSING": "CHAVE DO SITE ausente", - "ACCOUNT_NOT_FOUND": "Conta não encontrada" + "ACCOUNT_NOT_FOUND": "Conta não encontrada", + "INVALID_TIMESTAMP": "Carimbo de data/hora inválido" }, "CONTRACT": { "INVALID_METHOD": "Método de contrato inválido", @@ -85,7 +86,6 @@ "INVALID_SOLUTION_TYPE": "Tipo de solução inválido", "INVALID_ITEM_HASH": "Hash do item inválido", "DIFFERENT_DATASET_IDS": "Os IDs do conjunto de dados não correspondem", - "INVALID_TIMESTAMP": "Carimbo de data/hora inválido", "ID_MISMATCH": "Desajuste de ID do captcha", "MISSING_ITEM_HASH": "Hash do item ausente", "INVALID_CAPTCHA_CHALLENGE": "Desafio de captcha inválido", diff --git a/packages/locale/src/node/i18nMiddleware.ts b/packages/locale/src/node/i18nMiddleware.ts index 041ebc9241..fba483f0e7 100644 --- a/packages/locale/src/node/i18nMiddleware.ts +++ b/packages/locale/src/node/i18nMiddleware.ts @@ -1,3 +1,4 @@ +import React from "react"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/packages/procaptcha-bundle/src/util/language.ts b/packages/procaptcha-bundle/src/util/language.ts index e558a51bd6..d56f64c494 100644 --- a/packages/procaptcha-bundle/src/util/language.ts +++ b/packages/procaptcha-bundle/src/util/language.ts @@ -1,4 +1,3 @@ -import { LanguageSchema } from "@prosopo/locale"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,6 +11,8 @@ import { LanguageSchema } from "@prosopo/locale"; // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + +import { LanguageSchema } from "@prosopo/locale"; import type { ProcaptchaClientConfigInput, ProcaptchaRenderOptions, diff --git a/packages/procaptcha-common/src/state/builder.ts b/packages/procaptcha-common/src/state/builder.ts index fbef6ac4cc..fbc5484b6f 100644 --- a/packages/procaptcha-common/src/state/builder.ts +++ b/packages/procaptcha-common/src/state/builder.ts @@ -14,9 +14,9 @@ import type { Account, CaptchaResponseBody, + ProcaptchaApiInterface, ProcaptchaState, ProcaptchaStateUpdateFn, - ProsopoCaptchaApiInterface, TCaptchaSubmitResult, } from "@prosopo/types"; @@ -59,7 +59,7 @@ export const useProcaptcha = ( const [index, setIndex] = useState(0); const [solutions, setSolutions] = useState([] as string[][]); const [captchaApi, setCaptchaApi] = useRefAsState< - ProsopoCaptchaApiInterface | undefined + ProcaptchaApiInterface | undefined >(useRef, undefined); const [showModal, setShowModal] = useState(false); const [challenge, setChallenge] = useState( @@ -79,6 +79,7 @@ export const useProcaptcha = ( useRefAsState(useRef, undefined); const [sendData, setSendData] = useState(false); const [attemptCount, setAttemptCount] = useState(0); + const [error, setError] = useState(undefined); return [ // the state { @@ -96,6 +97,7 @@ export const useProcaptcha = ( successfullChallengeTimeout, sendData, attemptCount, + error, }, // and method to update the state (nextState: Partial) => { @@ -122,6 +124,7 @@ export const useProcaptcha = ( if (nextState.sendData !== undefined) setSendData(nextState.sendData); if (nextState.attemptCount !== undefined) setAttemptCount(nextState.attemptCount); + if (nextState.error !== undefined) setError(nextState.error); }, ]; }; diff --git a/packages/procaptcha-pow/src/components/ProcaptchaWidget.tsx b/packages/procaptcha-pow/src/components/ProcaptchaWidget.tsx index d17937d2a4..19aca13276 100644 --- a/packages/procaptcha-pow/src/components/ProcaptchaWidget.tsx +++ b/packages/procaptcha-pow/src/components/ProcaptchaWidget.tsx @@ -134,6 +134,7 @@ const Procaptcha = (props: ProcaptchaProps) => { onChange={manager.current.start} themeColor={themeColor} labelText={t("WIDGET.I_AM_HUMAN")} + error={state.error} aria-label="human checkbox" /> )} diff --git a/packages/procaptcha-pow/src/services/Manager.ts b/packages/procaptcha-pow/src/services/Manager.ts index 0b5db48103..32ea20f824 100644 --- a/packages/procaptcha-pow/src/services/Manager.ts +++ b/packages/procaptcha-pow/src/services/Manager.ts @@ -191,59 +191,66 @@ export const Manager = ( getDappAccount(), ); - const solution = solvePoW(challenge.challenge, challenge.difficulty); + if (challenge.error) { + updateState({ + error: challenge.error, + loading: false, + }); + } else { + const solution = solvePoW(challenge.challenge, challenge.difficulty); - const user = await ext.getAccount(getConfig()); + const user = await ext.getAccount(getConfig()); - const signer = user.extension?.signer; + const signer = user.extension?.signer; - if (!signer || !signer.signRaw) { - throw new ProsopoEnvError("GENERAL.CANT_FIND_KEYRINGPAIR", { - context: { - error: - "Signer is not defined, cannot sign message to prove account ownership", - }, - }); - } - - const userTimestampSignature = await signer.signRaw({ - address: userAccount, - data: stringToHex(challenge[ApiParams.timestamp].toString()), - type: "bytes", - }); + if (!signer || !signer.signRaw) { + throw new ProsopoEnvError("GENERAL.CANT_FIND_KEYRINGPAIR", { + context: { + error: + "Signer is not defined, cannot sign message to prove account ownership", + }, + }); + } - const verifiedSolution = await providerApi.submitPowCaptchaSolution( - challenge, - getAccount().account.account.address, - getDappAccount(), - solution, - userTimestampSignature.signature.toString(), - config.captchas.pow.verifiedTimeout, - ); - if (verifiedSolution[ApiParams.verified]) { - updateState({ - isHuman: true, - loading: false, + const userTimestampSignature = await signer.signRaw({ + address: userAccount, + data: stringToHex(challenge[ApiParams.timestamp].toString()), + type: "bytes", }); - events.onHuman( - encodeProcaptchaOutput({ - [ApiParams.providerUrl]: providerUrl, - [ApiParams.user]: getAccount().account.account.address, - [ApiParams.dapp]: getDappAccount(), - [ApiParams.challenge]: challenge.challenge, - [ApiParams.nonce]: solution, - [ApiParams.timestamp]: challenge.timestamp, - [ApiParams.signature]: { - [ApiParams.provider]: challenge.signature.provider, - [ApiParams.user]: { - [ApiParams.timestamp]: - userTimestampSignature.signature.toString(), - }, - }, - }), + const verifiedSolution = await providerApi.submitPowCaptchaSolution( + challenge, + getAccount().account.account.address, + getDappAccount(), + solution, + userTimestampSignature.signature.toString(), + config.captchas.pow.verifiedTimeout, ); - setValidChallengeTimeout(); + if (verifiedSolution[ApiParams.verified]) { + updateState({ + isHuman: true, + loading: false, + }); + + events.onHuman( + encodeProcaptchaOutput({ + [ApiParams.providerUrl]: providerUrl, + [ApiParams.user]: getAccount().account.account.address, + [ApiParams.dapp]: getDappAccount(), + [ApiParams.challenge]: challenge.challenge, + [ApiParams.nonce]: solution, + [ApiParams.timestamp]: challenge.timestamp, + [ApiParams.signature]: { + [ApiParams.provider]: challenge.signature.provider, + [ApiParams.user]: { + [ApiParams.timestamp]: + userTimestampSignature.signature.toString(), + }, + }, + }), + ); + setValidChallengeTimeout(); + } } }, start, diff --git a/packages/procaptcha-react/src/components/ProcaptchaWidget.tsx b/packages/procaptcha-react/src/components/ProcaptchaWidget.tsx index c0e897747f..ea19f3120c 100644 --- a/packages/procaptcha-react/src/components/ProcaptchaWidget.tsx +++ b/packages/procaptcha-react/src/components/ProcaptchaWidget.tsx @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + /** @jsxImportSource @emotion/react */ import { i18n, useTranslation } from "@prosopo/locale-browser"; @@ -117,7 +118,8 @@ const ProcaptchaWidget = (props: ProcaptchaProps) => { >
{ onChange={manager.start} checked={state.isHuman} labelText={t("WIDGET.I_AM_HUMAN")} + error={state.error} aria-label="human checkbox" />
diff --git a/packages/procaptcha/src/modules/Manager.ts b/packages/procaptcha/src/modules/Manager.ts index 664312bc8f..da7b04eaaa 100644 --- a/packages/procaptcha/src/modules/Manager.ts +++ b/packages/procaptcha/src/modules/Manager.ts @@ -39,7 +39,6 @@ import { ProcaptchaConfigSchema, type ProcaptchaState, type ProcaptchaStateUpdateFn, - type RandomProvider, type TCaptchaSubmitResult, encodeProcaptchaOutput, } from "@prosopo/types"; @@ -148,31 +147,38 @@ export function Manager( const challenge = await captchaApi.getCaptchaChallenge(); - if (challenge.captchas.length <= 0) { - throw new ProsopoDatasetError("DEVELOPER.PROVIDER_NO_CAPTCHA"); + if (challenge.error) { + updateState({ + loading: false, + error: challenge.error, + }); + } else { + if (challenge.captchas.length <= 0) { + throw new ProsopoDatasetError("DEVELOPER.PROVIDER_NO_CAPTCHA"); + } + + // setup timeout, taking the timeout from the individual captcha or the global default + const timeMillis: number = challenge.captchas + .map( + (captcha) => + captcha.timeLimitMs || config.captchas.image.challengeTimeout, + ) + .reduce((a: number, b: number) => a + b); + const timeout = setTimeout(() => { + events.onChallengeExpired(); + // expired, disallow user's claim to be human + updateState({ isHuman: false, showModal: false, loading: false }); + }, timeMillis); + + // update state with new challenge + updateState({ + index: 0, + solutions: challenge.captchas.map(() => []), + challenge, + showModal: true, + timeout, + }); } - - // setup timeout, taking the timeout from the individual captcha or the global default - const timeMillis: number = challenge.captchas - .map( - (captcha) => - captcha.timeLimitMs || config.captchas.image.challengeTimeout, - ) - .reduce((a: number, b: number) => a + b); - const timeout = setTimeout(() => { - events.onChallengeExpired(); - // expired, disallow user's claim to be human - updateState({ isHuman: false, showModal: false, loading: false }); - }, timeMillis); - - // update state with new challenge - updateState({ - index: 0, - solutions: challenge.captchas.map(() => []), - challenge, - showModal: true, - timeout, - }); }, start, resetState, diff --git a/packages/procaptcha/src/modules/ProsopoCaptchaApi.ts b/packages/procaptcha/src/modules/ProsopoCaptchaApi.ts index 6ee0a80d05..91aa778659 100644 --- a/packages/procaptcha/src/modules/ProsopoCaptchaApi.ts +++ b/packages/procaptcha/src/modules/ProsopoCaptchaApi.ts @@ -11,25 +11,25 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + import type { ContractSubmittableResult } from "@polkadot/api-contract/base/Contract"; -import type { Signer } from "@polkadot/api/types"; -import { stringToHex } from "@polkadot/util/string"; import type { ProviderApi } from "@prosopo/api"; import { ProsopoDatasetError, ProsopoEnvError } from "@prosopo/common"; import { CaptchaMerkleTree, computeCaptchaSolutionHash, } from "@prosopo/datasets"; -import type { - CaptchaResponseBody, - CaptchaSolution, - CaptchaSolutionResponse, - ProsopoCaptchaApiInterface, - RandomProvider, +import { + ApiParams, + type CaptchaResponseBody, + type CaptchaSolution, + type CaptchaSolutionResponse, + type ProcaptchaApiInterface, + type RandomProvider, } from "@prosopo/types"; import type { TCaptchaSubmitResult } from "@prosopo/types"; -export class ProsopoCaptchaApi implements ProsopoCaptchaApiInterface { +export class ProsopoCaptchaApi implements ProcaptchaApiInterface { userAccount: string; provider: RandomProvider; providerApi: ProviderApi; @@ -60,6 +60,11 @@ export class ProsopoCaptchaApi implements ProsopoCaptchaApiInterface { this.userAccount, this.provider, ); + + if (captchaChallenge[ApiParams.error]) { + return captchaChallenge; + } + // convert https/http to match page for (const captcha of captchaChallenge.captchas) { for (const item of captcha.items) { diff --git a/packages/provider/src/api/admin.ts b/packages/provider/src/api/admin.ts index 5fa54b4259..87864cc3e0 100644 --- a/packages/provider/src/api/admin.ts +++ b/packages/provider/src/api/admin.ts @@ -1,3 +1,4 @@ +import { Logger, logError } from "@prosopo/common"; // Copyright 2021-2024 Prosopo (UK) Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -11,24 +12,22 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { AdminApiPaths } from "@prosopo/types"; +import { + AdminApiPaths, + type ApiResponse, + VerifyPowCaptchaSolutionBody, +} from "@prosopo/types"; import type { ProviderEnvironment } from "@prosopo/types-env"; import { Router } from "express"; import { Tasks } from "../index.js"; import { authMiddleware } from "./authMiddleware.js"; -// Setting batch commit interval to 0 for API calls -const apiBatchCommitConfig = { - interval: 0, - maxBatchExtrinsicPercentage: 59, -}; - export function prosopoAdminRouter(env: ProviderEnvironment): Router { const router = Router(); const tasks = new Tasks(env); // Use the authMiddleware for all routes in this router - router.use(authMiddleware(tasks, env)); + router.use(authMiddleware(env)); router.post(AdminApiPaths.UpdateDataset, async (req, res, next) => { try { @@ -37,8 +36,22 @@ export function prosopoAdminRouter(env: ProviderEnvironment): Router { console.info(`Dataset update complete: ${result}`); res.status(200).send(result); } catch (err) { - console.error(err); - res.status(500).send(err); + logError(err, tasks.logger); + res.status(500).send("An internal server error occurred."); + } + }); + + router.post(AdminApiPaths.SiteKeyRegister, async (req, res, next) => { + try { + const parsed = VerifyPowCaptchaSolutionBody.parse(req.body); + await tasks.clientTaskManager.registerSiteKey(parsed.siteKey); + const response: ApiResponse = { + status: "success", + }; + res.json(response); + } catch (err) { + logError(err, tasks.logger); + res.status(500).send("An internal server error occurred."); } }); diff --git a/packages/provider/src/api/authMiddleware.ts b/packages/provider/src/api/authMiddleware.ts index aeebab5a82..8adc413976 100644 --- a/packages/provider/src/api/authMiddleware.ts +++ b/packages/provider/src/api/authMiddleware.ts @@ -18,17 +18,17 @@ import type { ProviderEnvironment } from "@prosopo/types-env"; import type { NextFunction, Request, Response } from "express"; import type { Tasks } from "../index.js"; -export const authMiddleware = (tasks: Tasks, env: ProviderEnvironment) => { +export const authMiddleware = (env: ProviderEnvironment) => { return async (req: Request, res: Response, next: NextFunction) => { try { - const { signature, blocknumber } = extractHeaders(req); + const { signature, timestamp } = extractHeaders(req); if (!env.pair) { throw new ProsopoEnvError("CONTRACT.CANNOT_FIND_KEYPAIR"); } verifyEnvironmentKeyPair(env); - verifySignature(signature, blocknumber, env.pair); + verifySignature(signature, timestamp, env.pair); next(); } catch (err) { @@ -40,9 +40,9 @@ export const authMiddleware = (tasks: Tasks, env: ProviderEnvironment) => { const extractHeaders = (req: Request) => { const signature = req.headers.signature as string; - const blocknumber = req.headers.blocknumber as string; + const timestamp = req.headers.timestamp as string; - if (!signature || !blocknumber) { + if (!signature || !timestamp) { throw new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT", { context: { error: "Missing signature or block number", code: 400 }, }); @@ -50,7 +50,7 @@ const extractHeaders = (req: Request) => { if ( Array.isArray(signature) || - Array.isArray(blocknumber) || + Array.isArray(timestamp) || !isHex(signature) ) { throw new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT", { @@ -58,7 +58,16 @@ const extractHeaders = (req: Request) => { }); } - return { signature, blocknumber }; + // check if timestamp is from the last 5 minutes + const now = new Date().getTime(); + const ts = Number.parseInt(timestamp, 10); + if (now - ts > 300000) { + throw new ProsopoApiError("GENERAL.INVALID_TIMESTAMP", { + context: { error: "Timestamp is too old", code: 400 }, + }); + } + + return { signature, timestamp }; }; const verifyEnvironmentKeyPair = (env: ProviderEnvironment) => { @@ -69,12 +78,12 @@ const verifyEnvironmentKeyPair = (env: ProviderEnvironment) => { export const verifySignature = ( signature: string, - blockNumber: string, + timestamp: string, pair: KeyringPair, ) => { const u8Sig = hexToU8a(signature); - if (!pair.verify(blockNumber, u8Sig, pair.publicKey)) { + if (!pair.verify(timestamp, u8Sig, pair.publicKey)) { throw new ProsopoApiError("GENERAL.INVALID_SIGNATURE", { context: { error: "Signature verification failed", code: 401 }, }); diff --git a/packages/provider/src/api/captcha.ts b/packages/provider/src/api/captcha.ts index 39f0878a30..bed2d48b45 100644 --- a/packages/provider/src/api/captcha.ts +++ b/packages/provider/src/api/captcha.ts @@ -31,7 +31,7 @@ import { type TGetImageCaptchaChallengePathAndParams, } from "@prosopo/types"; import type { ProviderEnvironment } from "@prosopo/types-env"; -import { version } from "@prosopo/util"; +import { flatten, version } from "@prosopo/util"; import express, { type Router } from "express"; import { Tasks } from "../tasks/tasks.js"; import { handleErrors } from "./errorHandler.js"; @@ -69,17 +69,28 @@ export function prosopoRouter(env: ProviderEnvironment): Router { const GetImageCaptchaChallengePath: TGetImageCaptchaChallengePathAndParams = `${ApiPaths.GetImageCaptchaChallenge}/:${ApiParams.datasetId}/:${ApiParams.user}/:${ApiParams.dapp}`; router.get(GetImageCaptchaChallengePath, async (req, res, next) => { try { - const { datasetId, user } = CaptchaRequestBody.parse(req.params); + const { datasetId, user, dapp } = CaptchaRequestBody.parse(req.params); validateAddress(user, false, 42); + validateAddress(dapp, false, 42); + + const clientRecord = await tasks.db.getClientRecord(dapp); + + if (!clientRecord) { + return res.json({ + error: req.i18n.t("API.SITE_KEY_NOT_REGISTERED"), + code: 200, + }); + } const taskData = await tasks.imgCaptchaManager.getRandomCaptchasAndRequestHash( datasetId, user, req.ip || NO_IP_ADDRESS, - flattenHeaders(req.headers), + flatten(req.headers, ","), ); const captchaResponse: CaptchaResponseBody = { + [ApiParams.status]: "ok", [ApiParams.captchas]: taskData.captchas.map((captcha: Captcha) => ({ ...captcha, items: captcha.items.map((item) => @@ -126,6 +137,15 @@ export function prosopoRouter(env: ProviderEnvironment): Router { } try { + const clientRecord = await tasks.db.getClientRecord(parsed.dapp); + + if (!clientRecord) { + return res.json({ + error: req.i18n.t("API.SITE_KEY_NOT_REGISTERED"), + code: 200, + }); + } + // TODO allow the dapp to override the length of time that the request hash is valid for const result: DappUserSolutionResult = await tasks.imgCaptchaManager.dappUserSolution( @@ -137,7 +157,7 @@ export function prosopoRouter(env: ProviderEnvironment): Router { Number.parseInt(parsed[ApiParams.timestamp]), parsed[ApiParams.signature].provider.requestHash, req.ip || NO_IP_ADDRESS, - flattenHeaders(req.headers), + flatten(req.headers, ","), ); const returnValue: CaptchaSolutionResponse = { @@ -167,12 +187,28 @@ export function prosopoRouter(env: ProviderEnvironment): Router { try { const { user, dapp } = GetPowCaptchaChallengeRequestBody.parse(req.body); + validateAddress(user, false, 42); + validateAddress(dapp, false, 42); + + const clientRecord = await tasks.db.getClientRecord(dapp); + + if (!clientRecord) { + return res.json({ + error: req.i18n.t("API.SITE_KEY_NOT_REGISTERED"), + code: 200, + }); + } + + // TODO do something with domains + const origin = req.headers.origin; if (!origin) { - throw new ProsopoApiError("API.BAD_REQUEST", { - context: { code: 400, error: "origin header not found" }, - }); + return next( + new ProsopoApiError("API.BAD_REQUEST", { + context: { error: "origin header not found", code: 400 }, + }), + ); } const challenge = await tasks.powCaptchaManager.getPowCaptchaChallenge( @@ -191,16 +227,17 @@ export function prosopoRouter(env: ProviderEnvironment): Router { challenge.difficulty, challenge.providerSignature, req.ip || NO_IP_ADDRESS, - flattenHeaders(req.headers), + flatten(req.headers, ","), ); const getPowCaptchaResponse: GetPowCaptchaResponse = { - challenge: challenge.challenge, - difficulty: challenge.difficulty, - timestamp: challenge.requestedAtTimestamp.toString(), - signature: { - provider: { - challenge: challenge.providerSignature, + [ApiParams.status]: "ok", + [ApiParams.challenge]: challenge.challenge, + [ApiParams.difficulty]: challenge.difficulty, + [ApiParams.timestamp]: challenge.requestedAtTimestamp.toString(), + [ApiParams.signature]: { + [ApiParams.provider]: { + [ApiParams.challenge]: challenge.providerSignature, }, }, }; @@ -227,8 +264,18 @@ export function prosopoRouter(env: ProviderEnvironment): Router { */ router.post(ApiPaths.SubmitPowCaptchaSolution, async (req, res, next) => { try { - const { challenge, difficulty, signature, nonce, verifiedTimeout } = + const { challenge, difficulty, signature, nonce, verifiedTimeout, dapp } = SubmitPowCaptchaSolutionBody.parse(req.body); + + const clientRecord = await tasks.db.getClientRecord(dapp); + + if (!clientRecord) { + return res.json({ + error: req.i18n.t("API.SITE_KEY_NOT_REGISTERED"), + code: 200, + }); + } + const verified = await tasks.powCaptchaManager.verifyPowCaptchaSolution( challenge, difficulty, @@ -237,9 +284,9 @@ export function prosopoRouter(env: ProviderEnvironment): Router { verifiedTimeout, signature.user.timestamp, req.ip || NO_IP_ADDRESS, - flattenHeaders(req.headers), + flatten(req.headers, ","), ); - const response: PowCaptchaSolutionResponse = { verified }; + const response: PowCaptchaSolutionResponse = { status: "ok", verified }; return res.json(response); } catch (err) { tasks.logger.error(err); diff --git a/packages/provider/src/tasks/client/clientTasks.ts b/packages/provider/src/tasks/client/clientTasks.ts index 6edd4f148d..db41bf67b8 100644 --- a/packages/provider/src/tasks/client/clientTasks.ts +++ b/packages/provider/src/tasks/client/clientTasks.ts @@ -195,4 +195,12 @@ export class ClientTaskManager { ); } } + + async registerSiteKey(siteKey: string): Promise { + await this.providerDB.updateClientRecords([ + { + account: siteKey, + } as ClientRecord, + ]); + } } diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts index e74bb00d1d..dbfe0b0bd9 100644 --- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts +++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts @@ -167,6 +167,7 @@ export class ImgCaptchaManager { * @param timestamp * @param providerRequestHashSignature * @param ipAddress + * @param headers * @return {Promise} result containing the contract event */ async dappUserSolution( diff --git a/packages/provider/src/tasks/powCaptcha/powTasks.ts b/packages/provider/src/tasks/powCaptcha/powTasks.ts index 47bea17205..77b47371de 100644 --- a/packages/provider/src/tasks/powCaptcha/powTasks.ts +++ b/packages/provider/src/tasks/powCaptcha/powTasks.ts @@ -76,6 +76,7 @@ export class PowCaptchaManager { * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha * @param {string} userTimestampSignature * @param ipAddress + * @param headers */ async verifyPowCaptchaSolution( challenge: PoWChallengeId, diff --git a/packages/provider/src/tests/integration/imgCaptcha.integration.test.ts b/packages/provider/src/tests/integration/imgCaptcha.integration.test.ts index e6bae8daf6..e44fe53c1f 100644 --- a/packages/provider/src/tests/integration/imgCaptcha.integration.test.ts +++ b/packages/provider/src/tests/integration/imgCaptcha.integration.test.ts @@ -24,12 +24,16 @@ import { type TGetImageCaptchaChallengeURL, } from "@prosopo/types"; import fetch from "node-fetch"; -import { describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; import { dummyUserAccount } from "./mocks/solvedTestCaptchas.js"; +import { registerSiteKey } from "./registerSitekey.js"; const solutions = datasetWithSolutionHashes; - const baseUrl = "http://localhost:9229"; +const dappAccount = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; +const userAccount = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"; +const unRegisteredDappAccount = + "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL"; const getSolvedCaptchas = ( captchas: Captcha[], @@ -54,9 +58,11 @@ const getSolvedCaptchas = ( describe("Image Captcha Integration Tests", () => { describe("GetImageCaptchaChallenge", () => { + beforeAll(async () => { + await registerSiteKey(dappAccount); + }); + it("should supply an image captcha challenge to a Dapp User", async () => { - const userAccount = "5EquBjgKx98VFyP9xVYeAUE2soNGBUbru7L9pXgdmSmrDrQp"; - const dappAccount = "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw"; const origin = "http://localhost"; const getImageCaptchaURL: TGetImageCaptchaChallengeURL = `${baseUrl}${ApiPaths.GetImageCaptchaChallenge}/${solutions.datasetId}/${userAccount}/${dappAccount}`; @@ -69,13 +75,31 @@ describe("Image Captcha Integration Tests", () => { }); expect(response.status).toBe(200); + const data = await response.json(); + expect(data).toHaveProperty("captchas"); }); + + it("should not supply an image captcha challenge to a Dapp User if the site key is not registered", async () => { + const origin = "http://localhost"; + const getImageCaptchaURL: TGetImageCaptchaChallengeURL = `${baseUrl}${ApiPaths.GetImageCaptchaChallenge}/${solutions.datasetId}/${userAccount}/${unRegisteredDappAccount}`; + + const response = await fetch(getImageCaptchaURL, { + method: "GET", + headers: { + "Content-Type": "application/json", + Origin: origin, + }, + }); + + expect(response.status).toBe(200); + const data = (await response.json()) as CaptchaResponseBody; + expect(data).toHaveProperty("error"); + expect(data.error).toBe("Site key not registered"); + }); + it("should fail if datasetID is incorrect", async () => { - const userAccount = "5EquBjgKx98VFyP9xVYeAUE2soNGBUbru7L9pXgdmSmrDrQp"; - const dappAccount = "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw"; const datasetId = "thewrongdsetId"; const getImageCaptchaURL: TGetImageCaptchaChallengeURL = `${baseUrl}${ApiPaths.GetImageCaptchaChallenge}/${datasetId}/${userAccount}/${dappAccount}`; - const response = await fetch(getImageCaptchaURL, { method: "GET", headers: { @@ -96,7 +120,6 @@ describe("Image Captcha Integration Tests", () => { ); const userAccount = dummyUserAccount.address; - const dappAccount = "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw"; const origin = "http://localhost"; const getImageCaptchaURL: TGetImageCaptchaChallengeURL = `${baseUrl}${ApiPaths.GetImageCaptchaChallenge}/${solutions.datasetId}/${userAccount}/${dappAccount}`; const response = await fetch(getImageCaptchaURL, { @@ -166,7 +189,6 @@ describe("Image Captcha Integration Tests", () => { }, ); const jsonRes = await solveThatCaptcha.json(); - console.log(jsonRes); const res = jsonRes as CaptchaSolutionResponse; expect(res.status).toBe("You correctly answered the captchas"); diff --git a/packages/provider/src/tests/integration/powCaptcha.integration.test.ts b/packages/provider/src/tests/integration/powCaptcha.integration.test.ts index 05310426fa..5204bc7843 100644 --- a/packages/provider/src/tests/integration/powCaptcha.integration.test.ts +++ b/packages/provider/src/tests/integration/powCaptcha.integration.test.ts @@ -24,15 +24,18 @@ import { type SubmitPowCaptchaSolutionBodyType, } from "@prosopo/types"; import fetch from "node-fetch"; -import { describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; import { dummyDappAccount, dummyUserAccount, } from "./mocks/solvedTestCaptchas.js"; +import { registerSiteKey } from "./registerSitekey.js"; // Define the endpoint path and base URL const baseUrl = "http://localhost:9229"; const getPowCaptchaChallengePath = ApiPaths.GetPowCaptchaChallenge; +const dappAccount = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; +const userAccount = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"; const bufferToHex = (buffer: Uint8Array): string => Array.from(buffer) @@ -77,9 +80,11 @@ const failPoW = (data: string, difficulty: number): number => { describe("PoW Integration Tests", () => { describe("GetPowCaptchaChallenge", () => { + beforeAll(async () => { + await registerSiteKey(dappAccount); + }); + it("should supply a PoW challenge to a Dapp User", async () => { - const userAccount = "userAddress"; - const dappAccount = "dappAddress"; const origin = "http://localhost"; const body: GetPowCaptchaChallengeRequestBodyType = { user: userAccount, @@ -88,6 +93,7 @@ describe("PoW Integration Tests", () => { const response = await fetch(`${baseUrl}${getPowCaptchaChallengePath}`, { method: "POST", headers: { + Connection: "close", "Content-Type": "application/json", Origin: origin, }, @@ -104,12 +110,10 @@ describe("PoW Integration Tests", () => { }); it("should return an error if origin header is not provided", async () => { - const userAccount = "userAddress"; - const dappAccount = "dappAddress"; - const response = await fetch(`${baseUrl}${getPowCaptchaChallengePath}`, { method: "POST", headers: { + Connection: "close", "Content-Type": "application/json", }, body: JSON.stringify({ user: userAccount, dapp: dappAccount }), @@ -132,7 +136,11 @@ describe("PoW Integration Tests", () => { "sr25519", 42, ); + const dappAccount = dappPair.address; + + await registerSiteKey(dappAccount); + const origin = "http://localhost"; const requestBody: GetPowCaptchaChallengeRequestBodyType = { user: userPair.address, @@ -143,6 +151,7 @@ describe("PoW Integration Tests", () => { { method: "POST", headers: { + Connection: "close", "Content-Type": "application/json", Origin: origin, }, @@ -179,6 +188,7 @@ describe("PoW Integration Tests", () => { { method: "POST", headers: { + Connection: "close", "Content-Type": "application/json", Origin: origin, }, @@ -202,7 +212,6 @@ describe("PoW Integration Tests", () => { 42, ); const userAccount = userPair.address; - const dappAccount = "dappAddress"; const origin = "http://localhost"; const captchaRes = await fetch( @@ -210,6 +219,7 @@ describe("PoW Integration Tests", () => { { method: "POST", headers: { + Connection: "close", "Content-Type": "application/json", Origin: origin, }, @@ -226,6 +236,7 @@ describe("PoW Integration Tests", () => { const verifiedTimeout = 120000; const dapp = "5C7bfXYwachNuvmasEFtWi9BMS41uBvo6KpYHVSQmad4nWzw"; + await registerSiteKey(dapp); const body: SubmitPowCaptchaSolutionBodyType = { challenge, difficulty, @@ -247,6 +258,7 @@ describe("PoW Integration Tests", () => { { method: "POST", headers: { + Connection: "close", "Content-Type": "application/json", }, body: JSON.stringify(body), @@ -259,5 +271,72 @@ describe("PoW Integration Tests", () => { expect(data).toHaveProperty("verified"); expect(data.verified).toBe(false); }); + + it("should return an error for an unregistered site key", async () => { + const userPair = await getPairAsync( + dummyUserAccount.seed, + undefined, + "sr25519", + 42, + ); + const userAccount = userPair.address; + const origin = "http://localhost"; + + const captchaRes = await fetch( + `${baseUrl}${getPowCaptchaChallengePath}`, + { + method: "POST", + headers: { + Connection: "close", + "Content-Type": "application/json", + Origin: origin, + }, + body: JSON.stringify({ user: userAccount, dapp: dappAccount }), + }, + ); + + const challengeBody = (await captchaRes.json()) as GetPowCaptchaResponse; + + const challenge = challengeBody.challenge; + const difficulty = challengeBody.difficulty; + const signature = challengeBody.signature; + const nonce = failPoW(challenge, difficulty); + const verifiedTimeout = 120000; + + const dapp = "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"; + const body: SubmitPowCaptchaSolutionBodyType = { + challenge, + difficulty, + [ApiParams.signature]: { + [ApiParams.provider]: signature[ApiParams.provider], + [ApiParams.user]: { + [ApiParams.timestamp]: u8aToHex( + userPair.sign(challengeBody[ApiParams.timestamp].toString()), + ), + }, + }, + nonce, + verifiedTimeout, + user: userPair.address, + dapp, + }; + const response = await fetch( + `${baseUrl}${ApiPaths.SubmitPowCaptchaSolution}`, + { + method: "POST", + headers: { + Connection: "close", + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }, + ); + + expect(response.status).toBe(200); + + const data = (await response.json()) as PowCaptchaSolutionResponse; + expect(data).toHaveProperty("error"); + expect(data.error).toBe("Site key not registered"); + }); }); }); diff --git a/packages/provider/src/tests/integration/registerSitekey.ts b/packages/provider/src/tests/integration/registerSitekey.ts new file mode 100644 index 0000000000..df3bc810ba --- /dev/null +++ b/packages/provider/src/tests/integration/registerSitekey.ts @@ -0,0 +1,35 @@ +// Copyright 2021-2024 Prosopo (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { ProviderDatabase } from "@prosopo/database"; +import type { ClientRecord } from "@prosopo/types-database"; + +export const registerSiteKey = async (siteKey: string): Promise => { + const username = process.env.PROSOPO_DATABASE_USERNAME || "root"; + const pw = process.env.PROSOPO_DATABASE_PASSWORD || "root"; + const host = process.env.PROSOPO_DATABASE_HOST || "localhost"; + const port = process.env.PROSOPO_DATABASE_PORT || 27017; + const db = new ProviderDatabase( + `mongodb://${username}:${pw}@${host}:${port}`, + process.env.PROSOPO_DATABASE_NAME || "prosopo", + process.env.PROSOPO_DATABASE_AUTH_SOURCE || "admin", + ); + await db.connect(); + console.log("Registering site key", siteKey); + await db.updateClientRecords([ + { + account: siteKey, + } as ClientRecord, + ]); + await db.connection?.close(); +}; diff --git a/packages/provider/src/tests/unit/api/authMiddleware.unit.test.ts b/packages/provider/src/tests/unit/api/authMiddleware.unit.test.ts index 2149c185e8..b554cad614 100644 --- a/packages/provider/src/tests/unit/api/authMiddleware.unit.test.ts +++ b/packages/provider/src/tests/unit/api/authMiddleware.unit.test.ts @@ -39,7 +39,7 @@ describe("authMiddleware", () => { const mockReq = { headers: { signature: "0x1234", - blocknumber: "1234", + timestamp: new Date().getTime(), }, } as unknown as Request; @@ -54,7 +54,7 @@ describe("authMiddleware", () => { vi.mocked(hexToU8a).mockReturnValue(new Uint8Array()); vi.mocked(mockPair.verify).mockReturnValue(true); - const middleware = authMiddleware(mockTasks, mockEnv); + const middleware = authMiddleware(mockEnv); await middleware(mockReq, mockRes, mockNext); expect(mockNext).toHaveBeenCalled(); @@ -65,7 +65,7 @@ describe("authMiddleware", () => { const mockReq = { headers: { signature: "0x1234", - blocknumber: "1234", + timestamp: new Date().getTime(), }, } as unknown as Request; @@ -80,7 +80,7 @@ describe("authMiddleware", () => { vi.mocked(hexToU8a).mockReturnValue(new Uint8Array()); vi.mocked(mockPair.verify).mockReturnValue(false); - const middleware = authMiddleware(mockTasks, mockEnv); + const middleware = authMiddleware(mockEnv); await middleware(mockReq, mockRes, mockNext); expect(mockNext).not.toHaveBeenCalled(); @@ -95,7 +95,7 @@ describe("authMiddleware", () => { const mockReq = { headers: { signature: "0x1234", - blocknumber: "1234", + timestamp: new Date().getTime(), }, } as unknown as Request; @@ -110,7 +110,7 @@ describe("authMiddleware", () => { pair: null, } as unknown as ProviderEnvironment; - const middleware = authMiddleware(mockTasks, invalidEnv); + const middleware = authMiddleware(invalidEnv); await middleware(mockReq, mockRes, mockNext); expect(mockNext).not.toHaveBeenCalled(); diff --git a/packages/provider/vite.test.config.ts b/packages/provider/vite.test.config.ts index d81cb32db7..76c9706dac 100644 --- a/packages/provider/vite.test.config.ts +++ b/packages/provider/vite.test.config.ts @@ -12,23 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import fs from "node:fs"; -import path from "node:path"; import { ViteTestConfig } from "@prosopo/config"; -import dotenv from "dotenv"; +import { loadEnv } from "@prosopo/dotenv"; -process.env.NODE_ENV = "test"; -// if .env.test exists at this level, use it, otherwise use the one at the root -const envFile = `.env.${process.env.NODE_ENV || "development"}`; -let envPath = envFile; -if (fs.existsSync(envFile)) { - envPath = path.resolve(envFile); -} else if (fs.existsSync(`../../${envFile}`)) { - envPath = path.resolve(`../../${envFile}`); -} else { - throw new Error(`No ${envFile} file found`); -} - -dotenv.config({ path: envPath }); +loadEnv(); export default ViteTestConfig; diff --git a/packages/types-database/src/types/mongo.ts b/packages/types-database/src/types/mongo.ts index 648057ef29..811ac87501 100644 --- a/packages/types-database/src/types/mongo.ts +++ b/packages/types-database/src/types/mongo.ts @@ -11,8 +11,8 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import type { Logger } from "@prosopo/common"; +import type { Logger } from "@prosopo/common"; import type { Connection } from "mongoose"; export interface IDatabase { diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index fd69e08dec..df8d864727 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -482,4 +482,6 @@ export interface IProviderDatabase extends IDatabase { ): Promise; updateClientRecords(clientRecords: ClientRecord[]): Promise; + + getClientRecord(account: string): Promise; } diff --git a/packages/types/src/api/params.ts b/packages/types/src/api/params.ts index dae4be7ca5..fbf23c58d2 100644 --- a/packages/types/src/api/params.ts +++ b/packages/types/src/api/params.ts @@ -37,4 +37,6 @@ export enum ApiParams { secret = "secret", timestamp = "timestamp", signature = "signature", + error = "error", + siteKey = "siteKey", } diff --git a/packages/types/src/config/config.ts b/packages/types/src/config/config.ts index d6cfa26091..d888a94e4d 100644 --- a/packages/types/src/config/config.ts +++ b/packages/types/src/config/config.ts @@ -254,6 +254,7 @@ export type ProsopoClientConfigOutput = output< >; const ThemeType = union([literal("light"), literal("dark")]); +export const LanguageSchema = z.enum(["en", "es", "pt", "pt-BR"]); export const ProcaptchaConfigSchema = ProsopoClientConfigSchema.and( object({ diff --git a/packages/types/src/datasets/captcha.ts b/packages/types/src/datasets/captcha.ts index 1ca06efcdf..ced1d4af58 100644 --- a/packages/types/src/datasets/captcha.ts +++ b/packages/types/src/datasets/captcha.ts @@ -116,6 +116,7 @@ export const TimestampSchema = zNumber(); export type Timestamp = zInfer; export type UserAccount = string; export type DappAccount = string; +export type DatasetID = string; export const POW_SEPARATOR = "___"; diff --git a/packages/types/src/procaptcha/api.ts b/packages/types/src/procaptcha/api.ts index ff2906d58e..571ee5338d 100644 --- a/packages/types/src/procaptcha/api.ts +++ b/packages/types/src/procaptcha/api.ts @@ -11,25 +11,18 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import type { Signer } from "@polkadot/api/types"; + import type { ProviderApiInterface } from "../api/index.js"; -import type { IProsopoCaptchaContract } from "../contract/index.js"; import type { CaptchaSolution } from "../datasets/index.js"; -import type { - CaptchaResponseBody, - GetPowCaptchaResponse, - PowCaptchaSolutionResponse, - RandomProvider, -} from "../provider/index.js"; +import type { CaptchaResponseBody, RandomProvider } from "../provider/index.js"; import type { TCaptchaSubmitResult } from "./client.js"; -export interface ProsopoCaptchaApiInterface { +export interface ProcaptchaApiInterface { userAccount: string; provider: RandomProvider; providerApi: ProviderApiInterface; dappAccount: string; web2: boolean; - getCaptchaChallenge(): Promise; submitCaptchaSolution( userRequestHashSignature: string, requestHash: string, @@ -37,4 +30,5 @@ export interface ProsopoCaptchaApiInterface { timestamp: string, providerRequestHashSignature: string, ): Promise; + getCaptchaChallenge(): Promise; } diff --git a/packages/types/src/procaptcha/manager.ts b/packages/types/src/procaptcha/manager.ts index 533782bfa7..b1d9110de0 100644 --- a/packages/types/src/procaptcha/manager.ts +++ b/packages/types/src/procaptcha/manager.ts @@ -18,7 +18,7 @@ import type { import { object } from "zod"; import { ApiParams } from "../api/index.js"; import type { CaptchaResponseBody } from "../provider/index.js"; -import type { ProsopoCaptchaApiInterface } from "./api.js"; +import type { ProcaptchaApiInterface } from "./api.js"; import type { TCaptchaSubmitResult } from "./client.js"; import { type ProcaptchaToken, ProcaptchaTokenSpec } from "./token.js"; /** @@ -61,7 +61,7 @@ export interface ProcaptchaState { isHuman: boolean; // is the user human? index: number; // the index of the captcha round currently being shown solutions: string[][]; // the solutions for each captcha round - captchaApi: ProsopoCaptchaApiInterface | undefined; // the captcha api instance for managing captcha challenge. undefined if not set up + captchaApi: ProcaptchaApiInterface | undefined; // the captcha api instance for managing captcha challenge. undefined if not set up challenge: CaptchaResponseBody | undefined; // the captcha challenge from the provider. undefined if not set up showModal: boolean; // whether to show the modal or not loading: boolean; // whether the captcha is loading or not @@ -72,6 +72,7 @@ export interface ProcaptchaState { successfullChallengeTimeout: NodeJS.Timeout | undefined; // the timer for the captcha challenge. undefined if not set sendData: boolean; // whether to trigger sending user event data (mouse, keyboard, touch) to the provider attemptCount: number; // Number of attempts to successfully complete captcha without errors + error: string | undefined; // any error message } /** diff --git a/packages/types/src/provider/api.ts b/packages/types/src/provider/api.ts index 5ee4501e68..e98d090a00 100644 --- a/packages/types/src/provider/api.ts +++ b/packages/types/src/provider/api.ts @@ -32,8 +32,11 @@ import { import { type Captcha, CaptchaSolutionSchema, + type DappAccount, + type DatasetID, type PoWChallengeId, PowChallengeIdSchema, + type UserAccount, } from "../datasets/index.js"; import { type ChallengeSignature, @@ -56,7 +59,7 @@ export enum ApiPaths { } export type TGetImageCaptchaChallengePathAndParams = - `${ApiPaths.GetImageCaptchaChallenge}/${string}/${string}/${string}`; + `${ApiPaths.GetImageCaptchaChallenge}/${DatasetID}/${UserAccount}/${DappAccount}`; export type TGetImageCaptchaChallengeURL = `${string}${TGetImageCaptchaChallengePathAndParams}`; @@ -68,10 +71,10 @@ export type TSubmitPowCaptchaSolutionURL = `${string}${ApiPaths.SubmitPowCaptchaSolution}`; export enum AdminApiPaths { - BatchCommit = "/v1/prosopo/provider/admin/batch", UpdateDataset = "/v1/prosopo/provider/admin/dataset", ProviderDeregister = "/v1/prosopo/provider/admin/deregister", ProviderUpdate = "/v1/prosopo/provider/admin/update", + SiteKeyRegister = "/v1/prosopo/provider/admin/sitekey/register", } export type CombinedApiPaths = ApiPaths | AdminApiPaths; @@ -87,8 +90,8 @@ export const ProviderDefaultRateLimits = { [ApiPaths.GetProviderStatus]: { windowMs: 60000, limit: 60 }, [ApiPaths.GetProviderDetails]: { windowMs: 60000, limit: 60 }, [ApiPaths.SubmitUserEvents]: { windowMs: 60000, limit: 60 }, - [AdminApiPaths.BatchCommit]: { windowMs: 60000, limit: 5 }, [AdminApiPaths.UpdateDataset]: { windowMs: 60000, limit: 5 }, + [AdminApiPaths.SiteKeyRegister]: { windowMs: 60000, limit: 5 }, [AdminApiPaths.ProviderDeregister]: { windowMs: 60000, limit: 1 }, [AdminApiPaths.ProviderUpdate]: { windowMs: 60000, limit: 5 }, }; @@ -153,9 +156,9 @@ export interface DappUserSolutionResult { [ApiParams.verified]: boolean; } -export interface CaptchaSolutionResponse extends DappUserSolutionResult { - [ApiParams.status]: string; -} +export interface CaptchaSolutionResponse + extends ApiResponse, + DappUserSolutionResult {} export interface CaptchaIdAndProof { captchaId: string; @@ -170,14 +173,14 @@ export const CaptchaRequestBody = object({ export type CaptchaRequestBodyType = zInfer; -export type CaptchaResponseBody = { +export interface CaptchaResponseBody extends ApiResponse { [ApiParams.captchas]: Captcha[]; [ApiParams.requestHash]: string; [ApiParams.timestamp]: string; [ApiParams.signature]: { [ApiParams.provider]: RequestHashSignature; }; -}; +} export const CaptchaSolutionBody = object({ [ApiParams.user]: string(), @@ -219,8 +222,12 @@ export interface ProviderRegistered { status: "Registered" | "Unregistered"; } -export interface VerificationResponse { +export interface ApiResponse { [ApiParams.status]: string; + [ApiParams.error]?: string; +} + +export interface VerificationResponse extends ApiResponse { [ApiParams.verified]: boolean; } @@ -228,7 +235,7 @@ export interface ImageVerificationResponse extends VerificationResponse { [ApiParams.commitmentId]?: Hash; } -export interface GetPowCaptchaResponse { +export interface GetPowCaptchaResponse extends ApiResponse { [ApiParams.challenge]: PoWChallengeId; [ApiParams.difficulty]: number; [ApiParams.timestamp]: string; @@ -237,8 +244,9 @@ export interface GetPowCaptchaResponse { }; } -export interface PowCaptchaSolutionResponse { +export interface PowCaptchaSolutionResponse extends ApiResponse { [ApiParams.verified]: boolean; + [ApiParams.error]?: string; } /** @@ -290,3 +298,7 @@ export const SubmitPowCaptchaSolutionBody = object({ export type SubmitPowCaptchaSolutionBodyType = zInfer< typeof SubmitPowCaptchaSolutionBody >; + +export const VerifyPowCaptchaSolutionBody = object({ + [ApiParams.siteKey]: string(), +}); diff --git a/packages/util/src/util.ts b/packages/util/src/util.ts index 9b94fa3398..527cc4f04a 100644 --- a/packages/util/src/util.ts +++ b/packages/util/src/util.ts @@ -20,8 +20,8 @@ export function getCurrentFileDirectory(url: string) { return new URL(url).pathname.split("/").slice(0, -1).join("/"); } -export const flatten = (obj: object, prefix = ""): Record => { - const flattenedObj: Record = {}; +export const flatten = (obj: object, prefix = ""): Record => { + const flattenedObj: Record = {}; for (const [key, value] of Object.entries(obj)) { if (value instanceof Object) { Object.assign(flattenedObj, flatten(value, `${prefix + key}.`)); diff --git a/packages/web-components/src/Checkbox.tsx b/packages/web-components/src/Checkbox.tsx index e12a783e78..d1be589ad0 100644 --- a/packages/web-components/src/Checkbox.tsx +++ b/packages/web-components/src/Checkbox.tsx @@ -26,6 +26,7 @@ interface CheckboxProps extends ButtonHTMLAttributes { checked: boolean; onChange: () => void; labelText: string; + error?: string; } const checkboxBefore = css`{ @@ -53,6 +54,10 @@ const baseStyle: CSSProperties = { const ID_LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const FAQ_LINK = process.env.PROSOPO_DOCS_URL + ? `${new URL(`${process.env.PROSOPO_DOCS_URL}/en/basics/faq/`).href}/` + : "https://docs.prosopo.io/en/basics/faq/"; + const generateRandomId = () => { return Array.from( { length: 8 }, @@ -65,6 +70,7 @@ export const Checkbox: React.FC = ({ onChange, checked, labelText, + error, }: CheckboxProps) => { const theme = useMemo( () => (themeColor === "light" ? lightTheme : darkTheme), @@ -105,19 +111,43 @@ export const Checkbox: React.FC = ({ checked={checked} style={checkboxStyle} /> - + {error ? ( + + ) : ( + + )} ); }; diff --git a/packages/web-components/src/theme.ts b/packages/web-components/src/theme.ts index b4399edeaa..7607b25162 100644 --- a/packages/web-components/src/theme.ts +++ b/packages/web-components/src/theme.ts @@ -38,6 +38,10 @@ export const lightTheme = { default: "#fff", contrastText: "#000", }, + error: { + main: "#f44336", + }, + grey, }, spacing: { @@ -57,6 +61,9 @@ export const darkTheme = { default: "#303030", contrastText: "#fff", }, + error: { + main: "#f44336", + }, grey, }, spacing: {