From debcceb5af8778fe4f5a0c602f65ee47691f404c Mon Sep 17 00:00:00 2001 From: Brian Donovan <1938+eventualbuddha@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:10:07 -0700 Subject: [PATCH] RAVE snapshot commit This commit bundles most of the work done on RAVE into a single commit to make rebasing on `vxsuite` easier. --- .circleci/config.yml | 765 +- .github/CODEOWNERS | 15 - .gitignore | 6 +- Cargo.lock | 4035 +++- Cargo.toml | 79 +- Procfile | 1 + README.md | 44 +- RustConfig | 6 + app.json | 18 + apps/rave-jx-terminal/Makefile | 40 + apps/rave-jx-terminal/backend/.env | 1 + ...029f9e35c697e4add1fbbcf34b3d6f8d265a7.json | 22 + ...860d35e10c7964256492604cd3beb21fa00a4.json | 23 + ...5fca2b6c88cd47fe5053eb67f1193b8775859.json | 19 + ...118734c6cace06acd1e0adc2487d6418b97cb.json | 21 + ...3a14a507f6927219b909fc023c35a3417aba6.json | 20 + ...ced219a016ea6216c37fb17cd6082d6a68b4b.json | 27 + ...5423bb2b4166184dd1e917ce03665aa23b33b.json | 22 + ...89d88a8224fa5ff6b35599125992a72dd6759.json | 56 + ...3dbdcd42acf8e1dd037900b9a017d663e081f.json | 44 + ...2df46a7ed16eb7b67bb3f51ed198a655797d9.json | 74 + ...78f43c6495ccfbc52509a8b6f28b3e286543c.json | 20 + ...90df8aeaa8355d9f5713b2c20e7b22ec0ff45.json | 98 + ...d54229b27e4235c24238959c688005a62638e.json | 20 + ...85c893461db917c79417fc9ba5773ebd0fb99.json | 56 + ...70babdc7d08893cc5a2bbe5f25d866c61bd38.json | 23 + ...dde95fbe04f49e96a20227cce3b7d6b439e8a.json | 23 + ...8bacbea4174e84ba25dbd53fe0aee023c2079.json | 23 + ...e1153a3168603ff1df6548ac3249b1eb159c0.json | 20 + ...b263cd63057b11c0e90a822ac92c592fa255f.json | 20 + ...617b2d34544fd063e7f47b3b2eced9c972711.json | 32 + ...14cba5470b4f364c123d6ea65ae1682b0eefe.json | 20 + ...0ea922ff0358241be760d2d34798e74d562c0.json | 56 + ...f3b6ecd45df1e8231262648a3e27b176b73bb.json | 22 + ...05ffb1055e49c4ea8ff9b25f41afc1d1c41e0.json | 44 + ...b026a88493f8a891332420a6a25e4e2a98e99.json | 22 + ...88a1adeeda2297e8b895c20d849b5e76e49e5.json | 23 + ...58ca99823e6507273b93a74d8c887f75b93cb.json | 22 + ...ef7b690619520d45316712dc6cff9e5d74907.json | 20 + ...0fcfe11a5232fb1a1b7740ee090600b6d0a7e.json | 23 + ...9d576e13ffcd2ef0dcbcc1027ff2cc7cb8818.json | 74 + ...8974775a18f59a4d7ed9f247877cf43f6322e.json | 80 + ...961ccba9af1a9ee0c4751c567aecf9b49d4de.json | 58 + ...908fac5d28f2049658fec150196890eda9fef.json | 22 + ...40c2f3d81e6158d7abe32c38f3e86a1347eff.json | 18 + apps/rave-jx-terminal/backend/Cargo.toml | 34 + .../backend/certs/DODJITCEMAILCA_63.cer | Bin 0 -> 1257 bytes .../db/migrations/20230705182146_init.sql | 90 + apps/rave-jx-terminal/backend/package.json | 20 + apps/rave-jx-terminal/backend/script/build | 8 + apps/rave-jx-terminal/backend/script/ci | 14 + apps/rave-jx-terminal/backend/script/lint | 8 + apps/rave-jx-terminal/backend/src/app.rs | 143 + apps/rave-jx-terminal/backend/src/cac.rs | 247 + apps/rave-jx-terminal/backend/src/config.rs | 38 + apps/rave-jx-terminal/backend/src/db.rs | 1346 ++ apps/rave-jx-terminal/backend/src/log.rs | 35 + apps/rave-jx-terminal/backend/src/main.rs | 61 + apps/rave-jx-terminal/backend/src/sync.rs | 211 + .../fixtures/electionFamousNames2021.json | 324 + .../fixtures/robert_aikins_sample_cert.pem | 29 + apps/rave-jx-terminal/frontend/.gitignore | 16 + apps/rave-jx-terminal/frontend/Cargo.toml | 29 + apps/rave-jx-terminal/frontend/Dioxus.toml | 51 + apps/rave-jx-terminal/frontend/README.md | 48 + apps/rave-jx-terminal/frontend/input.css | 3 + apps/rave-jx-terminal/frontend/package.json | 24 + .../frontend/public/styles.css | 853 + apps/rave-jx-terminal/frontend/script/build | 10 + apps/rave-jx-terminal/frontend/src/app.rs | 40 + .../components/election_configuration_cell.rs | 40 + .../frontend/src/components/mod.rs | 3 + apps/rave-jx-terminal/frontend/src/layout.rs | 37 + apps/rave-jx-terminal/frontend/src/main.rs | 18 + .../frontend/src/pages/ballots_page.rs | 242 + .../frontend/src/pages/elections_page.rs | 106 + .../frontend/src/pages/mod.rs | 7 + .../frontend/src/pages/voters_page.rs | 209 + apps/rave-jx-terminal/frontend/src/route.rs | 30 + .../frontend/src/util/file.rs | 9 + .../rave-jx-terminal/frontend/src/util/mod.rs | 2 + .../rave-jx-terminal/frontend/src/util/url.rs | 8 + .../frontend/tailwind.config.js | 13 + apps/rave-mark/Makefile | 35 + apps/rave-mark/backend/Makefile | 14 + apps/rave-mark/backend/package.json | 71 +- apps/rave-mark/backend/schema.sql | 122 + apps/rave-mark/backend/src/app.ts | 516 +- apps/rave-mark/backend/src/env.d.ts | 8 +- apps/rave-mark/backend/src/globals.ts | 48 +- apps/rave-mark/backend/src/index.ts | 28 +- apps/rave-mark/backend/src/mailing_label.ts | 386 + .../backend/src/rave_server_client.ts | 344 + apps/rave-mark/backend/src/server.test.ts | 21 +- apps/rave-mark/backend/src/server.ts | 126 +- apps/rave-mark/backend/src/store.test.ts | 73 + apps/rave-mark/backend/src/store.ts | 1100 + apps/rave-mark/backend/src/types/auth.ts | 33 + apps/rave-mark/backend/src/types/db.ts | 500 + apps/rave-mark/backend/src/types/sync.ts | 230 + apps/rave-mark/backend/src/workspace.test.ts | 10 + apps/rave-mark/backend/src/workspace.ts | 54 + apps/rave-mark/backend/test/app_helpers.ts | 76 +- apps/rave-mark/backend/tsconfig.build.json | 1 - apps/rave-mark/backend/tsconfig.json | 1 - apps/rave-mark/frontend/.eslintrc.json | 3 +- apps/rave-mark/frontend/.stylelintrc.js | 19 - apps/rave-mark/frontend/Makefile | 26 + apps/rave-mark/frontend/jest.config.js | 1 + apps/rave-mark/frontend/package.json | 70 +- .../frontend/prodserver/package.json | 2 +- .../frontend/prodserver/setupProxy.js | 8 +- .../public/seals/Alabama-StateSeal.svg | 1395 ++ .../public/seals/Alaska-StateSeal.svg | 4317 ++++ .../public/seals/Arizona-StateSeal.svg | 1599 ++ .../public/seals/Arkansas-StateSeal.svg | 1406 ++ .../public/seals/Colorado-StateSeal.svg | 2473 ++ .../public/seals/Connecticut-StateSeal.svg | 1607 ++ .../public/seals/Delaware-StateSeal.svg | 1370 ++ .../public/seals/Florida-StateSeal.svg | 3236 +++ .../public/seals/Georgia-StateSeal.svg | 3910 +++ .../public/seals/Great_Seal_of_Montana.svg | 4629 ++++ .../Great_Seal_of_the_State_of_Louisiana.svg | 1660 ++ .../public/seals/Hawaii-StateSeal.svg | 7328 ++++++ .../public/seals/Idaho-StateSeal-NARA.svg | 2211 ++ .../frontend/public/seals/Idaho-StateSeal.svg | 4862 ++++ .../public/seals/Illinois-StateSeal.svg | 3602 +++ .../public/seals/Indiana-StateSeal.svg | 3 + .../frontend/public/seals/Iowa-StateSeal.svg | 6576 +++++ .../public/seals/Kansas-StateSeal.svg | 7184 ++++++ .../public/seals/Kentucky-StateSeal.svg | 1707 ++ .../public/seals/Louisiana-StateSeal.svg | 3931 +++ .../frontend/public/seals/Maine-StateSeal.svg | 3810 +++ .../public/seals/Massachusetts-StateSeal.svg | 2627 ++ .../public/seals/Michigan-StateSeal.svg | 2247 ++ .../public/seals/Minnesota-StateSeal.svg | 2722 +++ .../public/seals/Missouri-StateSeal.svg | 3207 +++ .../public/seals/Montana-StateSeal-NARA.svg | 976 + .../public/seals/Nebraska-StateSeal.svg | 4967 ++++ .../public/seals/Nevada-StateSeal.svg | 5256 ++++ .../seals/NewHampshire-StateSeal-NARA.svg | 1077 + .../public/seals/NewYork-StateSeal.svg | 6436 +++++ .../seals/New_Hampshire_Moultonborough.svg | 9 + .../public/seals/NorthCarolina-StateSeal.svg | 4948 ++++ .../public/seals/NorthDakota-StateSeal.svg | 3253 +++ .../public/seals/Oregon-StateSeal.svg | 2447 ++ .../public/seals/Pennsylvania-StateSeal.svg | 1425 ++ .../public/seals/RhodeIsland-StateSeal.svg | 4631 ++++ .../frontend/public/seals/Sample-Seal.svg | 4 + .../frontend/public/seals/Seal_of_Alabama.svg | 7 + .../public/seals/Seal_of_Arkansas.svg | 1802 ++ .../public/seals/Seal_of_California.svg | 4644 ++++ .../public/seals/Seal_of_Colorado.svg | 880 + .../public/seals/Seal_of_Connecticut.svg | 439 + .../public/seals/Seal_of_Delaware.svg | 1075 + .../frontend/public/seals/Seal_of_Florida.svg | 6846 ++++++ .../frontend/public/seals/Seal_of_Georgia.svg | 673 + .../frontend/public/seals/Seal_of_Idaho.svg | 3461 +++ .../public/seals/Seal_of_Illinois.svg | 3362 +++ .../frontend/public/seals/Seal_of_Kansas.svg | 3765 +++ .../public/seals/Seal_of_Kentucky.svg | 3058 +++ .../public/seals/Seal_of_Louisiana.svg | 2015 ++ .../frontend/public/seals/Seal_of_Maine.svg | 1995 ++ .../seals/Seal_of_Maryland_%28reverse%29.svg | 5353 +++++ .../public/seals/Seal_of_Massachusetts.svg | 3134 +++ .../public/seals/Seal_of_Michigan.svg | 827 + ...l_of_Minnesota_%281858%E2%80%931971%29.svg | 1245 + .../seals/Seal_of_Mississippi_1879-2014.svg | 1788 ++ .../Seal_of_Mississippi_2014-present.svg | 1727 ++ .../public/seals/Seal_of_Mississippi_BW.svg | 14 + .../public/seals/Seal_of_Missouri.svg | 6626 +++++ .../public/seals/Seal_of_Nebraska.svg | 2388 ++ .../public/seals/Seal_of_New_Hampshire.svg | 1387 ++ .../public/seals/Seal_of_New_Jersey.svg | 2193 ++ .../public/seals/Seal_of_New_Mexico.svg | 3910 +++ .../public/seals/Seal_of_New_York.svg | 1610 ++ .../public/seals/Seal_of_North_Carolina.svg | 4656 ++++ ...Seal_of_North_Carolina_%281971-1984%29.svg | 4649 ++++ .../frontend/public/seals/Seal_of_Ohio.svg | 811 + .../seals/Seal_of_Ohio_%28Official%29.svg | 903 + .../public/seals/Seal_of_Oklahoma.svg | 4021 ++++ .../seals/Seal_of_Oklahoma_%28B%26W%29.svg | 3471 +++ .../frontend/public/seals/Seal_of_Oregon.svg | 2896 +++ .../public/seals/Seal_of_Pennsylvania.svg | 1063 + .../public/seals/Seal_of_Rhode_Island.svg | 3986 +++ .../public/seals/Seal_of_South_Carolina.svg | 1918 ++ ...al_of_South_Carolina_%28Alternative%29.svg | 2008 ++ .../Seal_of_South_Dakota_%28B%26W%29.svg | 3120 +++ .../public/seals/Seal_of_Tennessee.svg | 3113 +++ .../frontend/public/seals/Seal_of_Texas.svg | 1431 ++ .../frontend/public/seals/Seal_of_Utah.svg | 2814 +++ .../seals/Seal_of_Utah_%28Alternate%29.svg | 2542 ++ .../Seal_of_Utah_%28alternative%29_2011.svg | 2777 +++ ..._%28alternative%2C_enhanced_variant%29.svg | 19936 ++++++++++++++++ .../Seal_of_Utah_%28enhanced_variant%29.svg | 17069 +++++++++++++ .../public/seals/Seal_of_Virginia.svg | 3348 +++ .../public/seals/Seal_of_Washington.svg | 2016 ++ .../public/seals/Seal_of_West_Virginia.svg | 2536 ++ .../public/seals/Seal_of_Wisconsin.svg | 2794 +++ .../frontend/public/seals/Seal_of_Wyoming.svg | 821 + ...nnecticut_Department_of_Transportation.svg | 265 + ...se_of_Representatives_of_Massachusetts.svg | 802 + ..._Senate_of_Massachusetts_%28variant%29.svg | 806 + .../seals/Seal_of_the_State_of_Hawaii.svg | 3938 +++ .../public/seals/SouthDakota-StateSeal.svg | 5382 +++++ .../public/seals/State_Seal_of_Alaska.svg | 7795 ++++++ .../frontend/public/seals/Utah-StateSeal.svg | 2133 ++ .../public/seals/Vermont_state_seal.svg | 1621 ++ .../public/seals/Virginia-StateSeal.svg | 1320 + .../public/seals/Washington-StateSeal.svg | 2967 +++ .../public/seals/WestVirginia-StateSeal.svg | 6650 ++++++ .../public/seals/Wisconsin-StateSeal.svg | 2101 ++ .../public/seals/Wyoming-StateSeal.svg | 2225 ++ .../seals/state-of-hamilton-official-seal.svg | 7 + .../public/seals/vermont-state-seal-bw.svg | 26 + apps/rave-mark/frontend/script/build-stubs | 1 + .../src/__snapshots__/app.test.tsx.snap | 11 - apps/rave-mark/frontend/src/api.ts | 141 +- apps/rave-mark/frontend/src/app.test.tsx | 8 - apps/rave-mark/frontend/src/app.tsx | 37 +- apps/rave-mark/frontend/src/app_root.tsx | 23 + .../frontend/src/components/button_footer.tsx | 13 + .../components/display_settings_button.tsx | 28 + .../components/display_settings_manager.tsx | 42 + .../src/components/pin_pad_modal.test.tsx | 148 + .../frontend/src/components/pin_pad_modal.tsx | 101 + .../frontend/src/components/text_input.tsx | 144 + apps/rave-mark/frontend/src/env.d.ts | 6 + apps/rave-mark/frontend/src/globals.ts | 3 + apps/rave-mark/frontend/src/index.tsx | 12 +- .../no_card_screen.test.tsx.snap | 24 + .../src/screens/admin/dashboard_screen.tsx | 138 + .../frontend/src/screens/admin/index.tsx | 1 + .../src/screens/admin_flow_screen.tsx | 17 + .../frontend/src/screens/has_card_screen.tsx | 14 + .../src/screens/no_card_screen.test.tsx | 7 + .../frontend/src/screens/no_card_screen.tsx | 12 + .../src/screens/registration/index.tsx | 2 + .../src/screens/registration/start_screen.tsx | 143 + .../screens/registration/status_screen.tsx | 12 + .../src/screens/voter_flow_screen.tsx | 332 + .../screens/voting/already_voted_screen.tsx | 15 + .../frontend/src/screens/voting/index.tsx | 10 + .../src/screens/voting/mark_screen.tsx | 91 + .../src/screens/voting/post_vote_screen.tsx | 22 + .../screens/voting/print_ballot_screen.tsx | 66 + .../voting/print_mailing_label_screen.tsx | 14 + .../src/screens/voting/review_mark_screen.tsx | 48 + .../voting/review_onscreen_ballot_screen.tsx | 58 + .../voting/review_printed_ballot_screen.tsx | 34 + .../src/screens/voting/start_screen.tsx | 29 + .../src/screens/voting/submit_screen.tsx | 86 + apps/rave-mark/frontend/src/stubs/fs.ts | 299 + apps/rave-mark/frontend/test/setup.ts | 1 + apps/rave-mark/frontend/tsconfig.json | 1 - apps/rave-mark/frontend/vite.config.ts | 9 +- .../integration-testing/.eslintrc.json | 21 - apps/rave-mark/integration-testing/.gitignore | 30 - .../integration-testing/cypress.config.ts | 10 - .../cypress/e2e/example.cy.ts | 6 - .../cypress/fixtures/example.json | 5 - .../cypress/support/commands.ts | 25 - .../cypress/support/e2e.ts | 20 - .../integration-testing/package.json | 54 - .../integration-testing/tsconfig.json | 23 - apps/rave-scan/Makefile | 40 + apps/rave-scan/backend/.env | 1 + ...029f9e35c697e4add1fbbcf34b3d6f8d265a7.json | 22 + ...1f5bb664b4a8441efc280fe6e2dd73ea6dcb5.json | 50 + ...b91b807b83a877f36ce1148e9dc84055567bb.json | 58 + ...9552e44a2e72ac4082672453fe50d2ec9a959.json | 56 + ...5fca2b6c88cd47fe5053eb67f1193b8775859.json | 19 + ...3a14a507f6927219b909fc023c35a3417aba6.json | 20 + ...ced219a016ea6216c37fb17cd6082d6a68b4b.json | 27 + ...5423bb2b4166184dd1e917ce03665aa23b33b.json | 22 + ...432308c5dd00dc14898452f7c0c0146e23595.json | 44 + ...be3e2d3556da6ccc46afff1bfc76da84fe410.json | 56 + ...78f43c6495ccfbc52509a8b6f28b3e286543c.json | 20 + ...d54229b27e4235c24238959c688005a62638e.json | 20 + ...70babdc7d08893cc5a2bbe5f25d866c61bd38.json | 23 + ...075062e4412d1f2206ec3ecbcb79a3342c8da.json | 32 + ...dde95fbe04f49e96a20227cce3b7d6b439e8a.json | 23 + ...9711116126764a1c8f600d00ceed977a20766.json | 23 + ...e1153a3168603ff1df6548ac3249b1eb159c0.json | 20 + ...b263cd63057b11c0e90a822ac92c592fa255f.json | 20 + ...14cba5470b4f364c123d6ea65ae1682b0eefe.json | 20 + ...5383d238c4a5a53768269230063419162668d.json | 15 + ...b026a88493f8a891332420a6a25e4e2a98e99.json | 22 + ...231f7ed617b75c3dcd617270f37318012d00d.json | 14 + ...88a1adeeda2297e8b895c20d849b5e76e49e5.json | 23 + ...58ca99823e6507273b93a74d8c887f75b93cb.json | 22 + ...ef7b690619520d45316712dc6cff9e5d74907.json | 20 + ...0fcfe11a5232fb1a1b7740ee090600b6d0a7e.json | 23 + ...8974775a18f59a4d7ed9f247877cf43f6322e.json | 80 + ...77b57a4d983779f8fbe4f5b67535e5179c554.json | 56 + ...908fac5d28f2049658fec150196890eda9fef.json | 22 + ...67f7e9e45b608e0dbb0d25af47a8bdc34dcae.json | 15 + apps/rave-scan/backend/Cargo.toml | 35 + apps/rave-scan/backend/build.rs | 5 + .../db/migrations/20230705182146_init.sql | 90 + .../20230911215714_add_batches.down.sql | 1 + .../migrations/20230911215714_add_batches.sql | 7 + apps/rave-scan/backend/package.json | 21 + apps/rave-scan/backend/script/build | 8 + apps/rave-scan/backend/script/ci | 14 + apps/rave-scan/backend/script/lint | 8 + apps/rave-scan/backend/src/app.rs | 194 + apps/rave-scan/backend/src/config.rs | 38 + apps/rave-scan/backend/src/db.rs | 1013 + apps/rave-scan/backend/src/log.rs | 35 + apps/rave-scan/backend/src/main.rs | 61 + apps/rave-scan/backend/src/sheets.rs | 23 + apps/rave-scan/backend/src/sync.rs | 218 + .../fixtures/electionFamousNames2021.json | 324 + apps/rave-scan/frontend/.gitignore | 16 + apps/rave-scan/frontend/Cargo.toml | 22 + apps/rave-scan/frontend/Dioxus.toml | 45 + apps/rave-scan/frontend/LICENSE | 21 + apps/rave-scan/frontend/README.md | 40 + apps/rave-scan/frontend/input.css | 3 + apps/rave-scan/frontend/package.json | 24 + apps/rave-scan/frontend/public/styles.css | 725 + apps/rave-scan/frontend/script/build | 10 + apps/rave-scan/frontend/src/main.rs | 184 + apps/rave-scan/frontend/tailwind.config.js | 13 + libs/backend/src/cast_vote_records/index.ts | 1 + libs/ballot-encoder-rs/Cargo.toml | 16 + libs/ballot-encoder-rs/package.json | 16 + libs/ballot-encoder-rs/src/consts.rs | 6 + libs/ballot-encoder-rs/src/decode.rs | 385 + libs/ballot-encoder-rs/src/encode.rs | 385 + libs/ballot-encoder-rs/src/lib.rs | 9 + libs/ballot-encoder-rs/src/types.rs | 39 + libs/ballot-encoder-rs/src/util.rs | 9 + libs/ballot-encoder-rs/tests/encoding.rs | 219 + .../fixtures/electionFamousNames2021.json | 324 + .../tests/fixtures/encoded.rs | 320 + libs/ballot-encoder-rs/tests/fixtures/mod.rs | 9 + libs/ballot-encoder-rs/tests/util/mod.rs | 152 + libs/basics/src/errors.test.ts | 1 + libs/basics/src/errors.ts | 13 +- libs/central-scanner/Cargo.toml | 13 + libs/central-scanner/package.json | 16 + libs/central-scanner/src/fujitsu.rs | 280 + libs/central-scanner/src/lib.rs | 3 + libs/db/src/client.ts | 6 + .../bin/generate-circleci-config | 2 +- libs/monorepo-utils/package.json | 5 +- libs/monorepo-utils/src/circleci.ts | 2 + libs/monorepo-utils/src/vscode.ts | 7 +- libs/test-utils/src/auth.ts | 13 + libs/types-rs/Cargo.toml | 29 + libs/types-rs/package.json | 16 + libs/types-rs/src/ballot_card.rs | 194 + libs/types-rs/src/cdf/cvr.rs | 1570 ++ libs/types-rs/src/cdf/mod.rs | 1 + libs/types-rs/src/election.rs | 610 + libs/types-rs/src/geometry.rs | 230 + libs/types-rs/src/lib.rs | 8 + libs/types-rs/src/rave/client/input.rs | 76 + libs/types-rs/src/rave/client/mod.rs | 2 + libs/types-rs/src/rave/client/output.rs | 128 + libs/types-rs/src/rave/jx/mod.rs | 278 + libs/types-rs/src/rave/mod.rs | 157 + libs/types-rs/src/scan.rs | 22 + libs/types-rs/src/util.rs | 47 + libs/types-rs/src/votes.rs | 49 + libs/types/src/auth/auth.ts | 9 + libs/types/src/printing.ts | 2 + libs/ui-rs/Cargo.toml | 19 + libs/ui-rs/package.json | 15 + libs/ui-rs/src/button.rs | 48 + libs/ui-rs/src/date_or_datetime_cell.rs | 30 + libs/ui-rs/src/file_button.rs | 87 + libs/ui-rs/src/lib.rs | 10 + libs/ui-rs/src/table_cell.rs | 17 + libs/ui-rs/src/util/datetime.rs | 145 + libs/ui-rs/src/util/mod.rs | 1 + libs/ui/src/error_boundary.tsx | 1 + libs/utils/src/format.ts | 43 + mprocs.yaml | 44 + pnpm-lock.yaml | 2911 +-- pnpm-workspace.yaml | 3 + script/rave-setup | 251 + script/reset-db | 22 + .../validate-monorepo/validation/circleci.ts | 13 +- services/rave-server/.env | 1 + services/rave-server/.gitignore | 1 + ...2edb13102141d3ee1ce7ca52023f80ffce385.json | 70 + ...745c79ed939919cf78f46b45f47f92b068430.json | 22 + ...a58ee5a2f9b869c5122be4bfa98100fa8da9e.json | 22 + ...b95f6055b6ad49f95c746bc268d4be50515da.json | 22 + ...ce7fcc7da3214562a130aab92c688893e13f1.json | 21 + ...0c9f3752f4ad6c791a8f0d20d21bb94411cf1.json | 94 + ...a8959d85dfd6b59b32d41ca825f69f0de6d91.json | 15 + ...ecb89117a92ad9841709ff0643ffc64ccf867.json | 92 + ...65dbe785a97c9dcd8107a5a75a90aec57c889.json | 68 + ...2fc9bcd53e06cc70d84284625187c94d011ff.json | 50 + ...27ce9124483a96e9237c2f49dd2a7e52b3474.json | 25 + ...5486643767997cab278cf80d0ff68a9632c20.json | 70 + ...46170ca116ab80e8df4f5d3961723aee17c84.json | 52 + ...1c364a917d7844e40b206bf7836f716544740.json | 50 + ...964fd5858a2fa2649c656d5025e4de6c866ae.json | 18 + ...a892ad2820668b3fdb8b54ce44d322d61f544.json | 22 + ...e4129fbba214f4cfb4001eb19edfc4ab7c2ce.json | 32 + ...cd50f2c1ea4343ff236733172c1cf66916ec2.json | 21 + ...ab357bbe118fa193c4ccddb9ac0cd9cafe202.json | 68 + ...908fac5d28f2049658fec150196890eda9fef.json | 22 + ...40c2f3d81e6158d7abe32c38f3e86a1347eff.json | 18 + ...9ea3160d5e13c6c23bbbfdc7f3f32668b225f.json | 52 + services/rave-server/Cargo.toml | 30 + services/rave-server/Makefile | 24 + services/rave-server/build.rs | 5 + .../db/migrations/20230705182146_init.sql | 87 + services/rave-server/package.json | 21 + services/rave-server/script/build | 8 + services/rave-server/script/ci | 14 + services/rave-server/script/lint | 8 + services/rave-server/src/app.rs | 241 + services/rave-server/src/config.rs | 23 + services/rave-server/src/db.rs | 942 + services/rave-server/src/log.rs | 35 + services/rave-server/src/main.rs | 64 + vxsuite.code-workspace | 36 +- 424 files changed, 342966 insertions(+), 3880 deletions(-) delete mode 100644 .github/CODEOWNERS create mode 100644 Procfile create mode 100644 RustConfig create mode 100644 app.json create mode 100644 apps/rave-jx-terminal/Makefile create mode 100644 apps/rave-jx-terminal/backend/.env create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-17be83249f634be8bd73445f7f7860d35e10c7964256492604cd3beb21fa00a4.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-277f42342d8553dca3dbb0f3bfb118734c6cace06acd1e0adc2487d6418b97cb.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-3c3386a91882bc4440873ee682889d88a8224fa5ff6b35599125992a72dd6759.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-4a9e5cf9a12c0e30462d79706c83dbdcd42acf8e1dd037900b9a017d663e081f.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-4cd544bf82339d9d2707a1eaebb2df46a7ed16eb7b67bb3f51ed198a655797d9.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-596419db69532c6f5062ab2c42990df8aeaa8355d9f5713b2c20e7b22ec0ff45.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-61b98014c74924645df5ea236ae85c893461db917c79417fc9ba5773ebd0fb99.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-8e90840e2a175e86af47134988e8bacbea4174e84ba25dbd53fe0aee023c2079.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-a9d72c1006b4e9649cd11cfaff5617b2d34544fd063e7f47b3b2eced9c972711.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-c35748d038bb33658ba19143e130ea922ff0358241be760d2d34798e74d562c0.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-c3ec601aff31a4f3d0222c94ef6f3b6ecd45df1e8231262648a3e27b176b73bb.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-d4bd52b3e2c3961766a2120fded05ffb1055e49c4ea8ff9b25f41afc1d1c41e0.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-ea42b1235a28ccdf68b6407099b9d576e13ffcd2ef0dcbcc1027ff2cc7cb8818.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-ed3276024209217e2f739174f2b961ccba9af1a9ee0c4751c567aecf9b49d4de.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json create mode 100644 apps/rave-jx-terminal/backend/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json create mode 100644 apps/rave-jx-terminal/backend/Cargo.toml create mode 100644 apps/rave-jx-terminal/backend/certs/DODJITCEMAILCA_63.cer create mode 100644 apps/rave-jx-terminal/backend/db/migrations/20230705182146_init.sql create mode 100644 apps/rave-jx-terminal/backend/package.json create mode 100755 apps/rave-jx-terminal/backend/script/build create mode 100755 apps/rave-jx-terminal/backend/script/ci create mode 100755 apps/rave-jx-terminal/backend/script/lint create mode 100644 apps/rave-jx-terminal/backend/src/app.rs create mode 100644 apps/rave-jx-terminal/backend/src/cac.rs create mode 100644 apps/rave-jx-terminal/backend/src/config.rs create mode 100644 apps/rave-jx-terminal/backend/src/db.rs create mode 100644 apps/rave-jx-terminal/backend/src/log.rs create mode 100644 apps/rave-jx-terminal/backend/src/main.rs create mode 100644 apps/rave-jx-terminal/backend/src/sync.rs create mode 100755 apps/rave-jx-terminal/backend/tests/fixtures/electionFamousNames2021.json create mode 100644 apps/rave-jx-terminal/backend/tests/fixtures/robert_aikins_sample_cert.pem create mode 100644 apps/rave-jx-terminal/frontend/.gitignore create mode 100644 apps/rave-jx-terminal/frontend/Cargo.toml create mode 100644 apps/rave-jx-terminal/frontend/Dioxus.toml create mode 100644 apps/rave-jx-terminal/frontend/README.md create mode 100644 apps/rave-jx-terminal/frontend/input.css create mode 100644 apps/rave-jx-terminal/frontend/package.json create mode 100644 apps/rave-jx-terminal/frontend/public/styles.css create mode 100755 apps/rave-jx-terminal/frontend/script/build create mode 100644 apps/rave-jx-terminal/frontend/src/app.rs create mode 100644 apps/rave-jx-terminal/frontend/src/components/election_configuration_cell.rs create mode 100644 apps/rave-jx-terminal/frontend/src/components/mod.rs create mode 100644 apps/rave-jx-terminal/frontend/src/layout.rs create mode 100644 apps/rave-jx-terminal/frontend/src/main.rs create mode 100644 apps/rave-jx-terminal/frontend/src/pages/ballots_page.rs create mode 100644 apps/rave-jx-terminal/frontend/src/pages/elections_page.rs create mode 100644 apps/rave-jx-terminal/frontend/src/pages/mod.rs create mode 100644 apps/rave-jx-terminal/frontend/src/pages/voters_page.rs create mode 100644 apps/rave-jx-terminal/frontend/src/route.rs create mode 100644 apps/rave-jx-terminal/frontend/src/util/file.rs create mode 100644 apps/rave-jx-terminal/frontend/src/util/mod.rs create mode 100644 apps/rave-jx-terminal/frontend/src/util/url.rs create mode 100644 apps/rave-jx-terminal/frontend/tailwind.config.js create mode 100644 apps/rave-mark/Makefile create mode 100644 apps/rave-mark/backend/Makefile create mode 100644 apps/rave-mark/backend/src/mailing_label.ts create mode 100644 apps/rave-mark/backend/src/rave_server_client.ts create mode 100644 apps/rave-mark/backend/src/store.test.ts create mode 100644 apps/rave-mark/backend/src/store.ts create mode 100644 apps/rave-mark/backend/src/types/auth.ts create mode 100644 apps/rave-mark/backend/src/types/db.ts create mode 100644 apps/rave-mark/backend/src/types/sync.ts create mode 100644 apps/rave-mark/backend/src/workspace.test.ts create mode 100644 apps/rave-mark/backend/src/workspace.ts delete mode 100644 apps/rave-mark/frontend/.stylelintrc.js create mode 100644 apps/rave-mark/frontend/Makefile create mode 100644 apps/rave-mark/frontend/public/seals/Alabama-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Alaska-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Arizona-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Arkansas-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Colorado-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Connecticut-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Delaware-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Florida-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Georgia-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Great_Seal_of_Montana.svg create mode 100644 apps/rave-mark/frontend/public/seals/Great_Seal_of_the_State_of_Louisiana.svg create mode 100644 apps/rave-mark/frontend/public/seals/Hawaii-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Idaho-StateSeal-NARA.svg create mode 100644 apps/rave-mark/frontend/public/seals/Idaho-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Illinois-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Indiana-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Iowa-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Kansas-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Kentucky-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Louisiana-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Maine-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Massachusetts-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Michigan-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Minnesota-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Missouri-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Montana-StateSeal-NARA.svg create mode 100644 apps/rave-mark/frontend/public/seals/Nebraska-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Nevada-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/NewHampshire-StateSeal-NARA.svg create mode 100644 apps/rave-mark/frontend/public/seals/NewYork-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/New_Hampshire_Moultonborough.svg create mode 100644 apps/rave-mark/frontend/public/seals/NorthCarolina-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/NorthDakota-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Oregon-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Pennsylvania-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/RhodeIsland-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Sample-Seal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Alabama.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Arkansas.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_California.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Colorado.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Connecticut.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Delaware.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Florida.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Georgia.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Idaho.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Illinois.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Kansas.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Kentucky.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Louisiana.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Maine.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Maryland_%28reverse%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Massachusetts.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Michigan.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Minnesota_%281858%E2%80%931971%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_1879-2014.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_2014-present.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_BW.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Missouri.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Nebraska.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_New_Hampshire.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_New_Jersey.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_New_Mexico.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_New_York.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina_%281971-1984%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Ohio.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Ohio_%28Official%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma_%28B%26W%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Oregon.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Pennsylvania.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Rhode_Island.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina_%28Alternative%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_South_Dakota_%28B%26W%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Tennessee.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Texas.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Utah.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28Alternate%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%29_2011.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%2C_enhanced_variant%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28enhanced_variant%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Virginia.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Washington.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_West_Virginia.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Wisconsin.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_Wyoming.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_the_Connecticut_Department_of_Transportation.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_the_House_of_Representatives_of_Massachusetts.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_the_Senate_of_Massachusetts_%28variant%29.svg create mode 100644 apps/rave-mark/frontend/public/seals/Seal_of_the_State_of_Hawaii.svg create mode 100644 apps/rave-mark/frontend/public/seals/SouthDakota-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/State_Seal_of_Alaska.svg create mode 100644 apps/rave-mark/frontend/public/seals/Utah-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Vermont_state_seal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Virginia-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Washington-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/WestVirginia-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Wisconsin-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/Wyoming-StateSeal.svg create mode 100644 apps/rave-mark/frontend/public/seals/state-of-hamilton-official-seal.svg create mode 100644 apps/rave-mark/frontend/public/seals/vermont-state-seal-bw.svg create mode 120000 apps/rave-mark/frontend/script/build-stubs delete mode 100644 apps/rave-mark/frontend/src/__snapshots__/app.test.tsx.snap delete mode 100644 apps/rave-mark/frontend/src/app.test.tsx create mode 100644 apps/rave-mark/frontend/src/app_root.tsx create mode 100644 apps/rave-mark/frontend/src/components/button_footer.tsx create mode 100644 apps/rave-mark/frontend/src/components/display_settings_button.tsx create mode 100644 apps/rave-mark/frontend/src/components/display_settings_manager.tsx create mode 100644 apps/rave-mark/frontend/src/components/pin_pad_modal.test.tsx create mode 100644 apps/rave-mark/frontend/src/components/pin_pad_modal.tsx create mode 100644 apps/rave-mark/frontend/src/components/text_input.tsx create mode 100644 apps/rave-mark/frontend/src/env.d.ts create mode 100644 apps/rave-mark/frontend/src/globals.ts create mode 100644 apps/rave-mark/frontend/src/screens/__snapshots__/no_card_screen.test.tsx.snap create mode 100644 apps/rave-mark/frontend/src/screens/admin/dashboard_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/admin/index.tsx create mode 100644 apps/rave-mark/frontend/src/screens/admin_flow_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/has_card_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/no_card_screen.test.tsx create mode 100644 apps/rave-mark/frontend/src/screens/no_card_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/registration/index.tsx create mode 100644 apps/rave-mark/frontend/src/screens/registration/start_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/registration/status_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voter_flow_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/already_voted_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/index.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/mark_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/post_vote_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/print_ballot_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/print_mailing_label_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/review_mark_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/review_onscreen_ballot_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/review_printed_ballot_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/start_screen.tsx create mode 100644 apps/rave-mark/frontend/src/screens/voting/submit_screen.tsx create mode 100644 apps/rave-mark/frontend/src/stubs/fs.ts create mode 100644 apps/rave-mark/frontend/test/setup.ts delete mode 100644 apps/rave-mark/integration-testing/.eslintrc.json delete mode 100644 apps/rave-mark/integration-testing/.gitignore delete mode 100644 apps/rave-mark/integration-testing/cypress.config.ts delete mode 100644 apps/rave-mark/integration-testing/cypress/e2e/example.cy.ts delete mode 100644 apps/rave-mark/integration-testing/cypress/fixtures/example.json delete mode 100644 apps/rave-mark/integration-testing/cypress/support/commands.ts delete mode 100644 apps/rave-mark/integration-testing/cypress/support/e2e.ts delete mode 100644 apps/rave-mark/integration-testing/package.json delete mode 100644 apps/rave-mark/integration-testing/tsconfig.json create mode 100644 apps/rave-scan/Makefile create mode 100644 apps/rave-scan/backend/.env create mode 100644 apps/rave-scan/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json create mode 100644 apps/rave-scan/backend/.sqlx/query-0db11db12dc749b20f699f8dab01f5bb664b4a8441efc280fe6e2dd73ea6dcb5.json create mode 100644 apps/rave-scan/backend/.sqlx/query-168c524a19a63caf753903e070ab91b807b83a877f36ce1148e9dc84055567bb.json create mode 100644 apps/rave-scan/backend/.sqlx/query-1e4cd90eb6b0ee7010004c2ea399552e44a2e72ac4082672453fe50d2ec9a959.json create mode 100644 apps/rave-scan/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json create mode 100644 apps/rave-scan/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json create mode 100644 apps/rave-scan/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json create mode 100644 apps/rave-scan/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json create mode 100644 apps/rave-scan/backend/.sqlx/query-389d4aa3005b51684781c3ce06a432308c5dd00dc14898452f7c0c0146e23595.json create mode 100644 apps/rave-scan/backend/.sqlx/query-39ea1da21f3ada5b21f0cf12bffbe3e2d3556da6ccc46afff1bfc76da84fe410.json create mode 100644 apps/rave-scan/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json create mode 100644 apps/rave-scan/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json create mode 100644 apps/rave-scan/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json create mode 100644 apps/rave-scan/backend/.sqlx/query-6fb997eb419320f00ff0e279e62075062e4412d1f2206ec3ecbcb79a3342c8da.json create mode 100644 apps/rave-scan/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json create mode 100644 apps/rave-scan/backend/.sqlx/query-869dcee67d4ee197b272b86b2ca9711116126764a1c8f600d00ceed977a20766.json create mode 100644 apps/rave-scan/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json create mode 100644 apps/rave-scan/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json create mode 100644 apps/rave-scan/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json create mode 100644 apps/rave-scan/backend/.sqlx/query-ca8f21b8ec1238ad97fe75692ce5383d238c4a5a53768269230063419162668d.json create mode 100644 apps/rave-scan/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json create mode 100644 apps/rave-scan/backend/.sqlx/query-dc16fad7a566eaf2ca2e567c723231f7ed617b75c3dcd617270f37318012d00d.json create mode 100644 apps/rave-scan/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json create mode 100644 apps/rave-scan/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json create mode 100644 apps/rave-scan/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json create mode 100644 apps/rave-scan/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json create mode 100644 apps/rave-scan/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json create mode 100644 apps/rave-scan/backend/.sqlx/query-f20f9b3ac334686762f3bddcac077b57a4d983779f8fbe4f5b67535e5179c554.json create mode 100644 apps/rave-scan/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json create mode 100644 apps/rave-scan/backend/.sqlx/query-fe73064275b12c3a341c87f926967f7e9e45b608e0dbb0d25af47a8bdc34dcae.json create mode 100644 apps/rave-scan/backend/Cargo.toml create mode 100644 apps/rave-scan/backend/build.rs create mode 100644 apps/rave-scan/backend/db/migrations/20230705182146_init.sql create mode 100644 apps/rave-scan/backend/db/migrations/20230911215714_add_batches.down.sql create mode 100644 apps/rave-scan/backend/db/migrations/20230911215714_add_batches.sql create mode 100644 apps/rave-scan/backend/package.json create mode 100755 apps/rave-scan/backend/script/build create mode 100755 apps/rave-scan/backend/script/ci create mode 100755 apps/rave-scan/backend/script/lint create mode 100644 apps/rave-scan/backend/src/app.rs create mode 100644 apps/rave-scan/backend/src/config.rs create mode 100644 apps/rave-scan/backend/src/db.rs create mode 100644 apps/rave-scan/backend/src/log.rs create mode 100644 apps/rave-scan/backend/src/main.rs create mode 100644 apps/rave-scan/backend/src/sheets.rs create mode 100644 apps/rave-scan/backend/src/sync.rs create mode 100755 apps/rave-scan/backend/tests/fixtures/electionFamousNames2021.json create mode 100644 apps/rave-scan/frontend/.gitignore create mode 100644 apps/rave-scan/frontend/Cargo.toml create mode 100644 apps/rave-scan/frontend/Dioxus.toml create mode 100644 apps/rave-scan/frontend/LICENSE create mode 100644 apps/rave-scan/frontend/README.md create mode 100644 apps/rave-scan/frontend/input.css create mode 100644 apps/rave-scan/frontend/package.json create mode 100644 apps/rave-scan/frontend/public/styles.css create mode 100755 apps/rave-scan/frontend/script/build create mode 100644 apps/rave-scan/frontend/src/main.rs create mode 100644 apps/rave-scan/frontend/tailwind.config.js create mode 100644 libs/ballot-encoder-rs/Cargo.toml create mode 100644 libs/ballot-encoder-rs/package.json create mode 100644 libs/ballot-encoder-rs/src/consts.rs create mode 100644 libs/ballot-encoder-rs/src/decode.rs create mode 100644 libs/ballot-encoder-rs/src/encode.rs create mode 100644 libs/ballot-encoder-rs/src/lib.rs create mode 100644 libs/ballot-encoder-rs/src/types.rs create mode 100644 libs/ballot-encoder-rs/src/util.rs create mode 100644 libs/ballot-encoder-rs/tests/encoding.rs create mode 100755 libs/ballot-encoder-rs/tests/fixtures/electionFamousNames2021.json create mode 100644 libs/ballot-encoder-rs/tests/fixtures/encoded.rs create mode 100644 libs/ballot-encoder-rs/tests/fixtures/mod.rs create mode 100644 libs/ballot-encoder-rs/tests/util/mod.rs create mode 100644 libs/central-scanner/Cargo.toml create mode 100644 libs/central-scanner/package.json create mode 100644 libs/central-scanner/src/fujitsu.rs create mode 100644 libs/central-scanner/src/lib.rs create mode 100644 libs/types-rs/Cargo.toml create mode 100644 libs/types-rs/package.json create mode 100644 libs/types-rs/src/ballot_card.rs create mode 100644 libs/types-rs/src/cdf/cvr.rs create mode 100644 libs/types-rs/src/cdf/mod.rs create mode 100644 libs/types-rs/src/election.rs create mode 100644 libs/types-rs/src/geometry.rs create mode 100644 libs/types-rs/src/lib.rs create mode 100644 libs/types-rs/src/rave/client/input.rs create mode 100644 libs/types-rs/src/rave/client/mod.rs create mode 100644 libs/types-rs/src/rave/client/output.rs create mode 100644 libs/types-rs/src/rave/jx/mod.rs create mode 100644 libs/types-rs/src/rave/mod.rs create mode 100644 libs/types-rs/src/scan.rs create mode 100644 libs/types-rs/src/util.rs create mode 100644 libs/types-rs/src/votes.rs create mode 100644 libs/ui-rs/Cargo.toml create mode 100644 libs/ui-rs/package.json create mode 100644 libs/ui-rs/src/button.rs create mode 100644 libs/ui-rs/src/date_or_datetime_cell.rs create mode 100644 libs/ui-rs/src/file_button.rs create mode 100644 libs/ui-rs/src/lib.rs create mode 100644 libs/ui-rs/src/table_cell.rs create mode 100644 libs/ui-rs/src/util/datetime.rs create mode 100644 libs/ui-rs/src/util/mod.rs create mode 100644 mprocs.yaml create mode 100755 script/rave-setup create mode 100755 script/reset-db create mode 100644 services/rave-server/.env create mode 100644 services/rave-server/.gitignore create mode 100644 services/rave-server/.sqlx/query-088f2a385709494b30d8b9c4a792edb13102141d3ee1ce7ca52023f80ffce385.json create mode 100644 services/rave-server/.sqlx/query-0908a3548f4c094b729f06d9645745c79ed939919cf78f46b45f47f92b068430.json create mode 100644 services/rave-server/.sqlx/query-1166fa780d90967db6362468c31a58ee5a2f9b869c5122be4bfa98100fa8da9e.json create mode 100644 services/rave-server/.sqlx/query-1ad99291100c5835132b78d0299b95f6055b6ad49f95c746bc268d4be50515da.json create mode 100644 services/rave-server/.sqlx/query-3548e96944f55ebcba37b8e7ec9ce7fcc7da3214562a130aab92c688893e13f1.json create mode 100644 services/rave-server/.sqlx/query-41a2ba726f24136aa7905660b200c9f3752f4ad6c791a8f0d20d21bb94411cf1.json create mode 100644 services/rave-server/.sqlx/query-4da37d9febc69ce401de6664a86a8959d85dfd6b59b32d41ca825f69f0de6d91.json create mode 100644 services/rave-server/.sqlx/query-67684dc852bf3c5313916385f6aecb89117a92ad9841709ff0643ffc64ccf867.json create mode 100644 services/rave-server/.sqlx/query-77c583ca83f68a91e7ba536df8665dbe785a97c9dcd8107a5a75a90aec57c889.json create mode 100644 services/rave-server/.sqlx/query-78b992e6d2f51d7eb96191b53172fc9bcd53e06cc70d84284625187c94d011ff.json create mode 100644 services/rave-server/.sqlx/query-8c0929b010ff6dcd3b56635f15a27ce9124483a96e9237c2f49dd2a7e52b3474.json create mode 100644 services/rave-server/.sqlx/query-9bd37a8ad15e621157a5899580f5486643767997cab278cf80d0ff68a9632c20.json create mode 100644 services/rave-server/.sqlx/query-a721d3d9e7500562a5c4dea6f4f46170ca116ab80e8df4f5d3961723aee17c84.json create mode 100644 services/rave-server/.sqlx/query-bc48af17fc906f194c3c452a5191c364a917d7844e40b206bf7836f716544740.json create mode 100644 services/rave-server/.sqlx/query-c72d6ec45e4ebc48188aee36c4e964fd5858a2fa2649c656d5025e4de6c866ae.json create mode 100644 services/rave-server/.sqlx/query-d66db8b2deaf5c944481b903ec5a892ad2820668b3fdb8b54ce44d322d61f544.json create mode 100644 services/rave-server/.sqlx/query-d9d643718503989d188c6c78e4fe4129fbba214f4cfb4001eb19edfc4ab7c2ce.json create mode 100644 services/rave-server/.sqlx/query-e2603c8a2dda98dca20f1236bd9cd50f2c1ea4343ff236733172c1cf66916ec2.json create mode 100644 services/rave-server/.sqlx/query-f305266bf89a3cab405a359d5f0ab357bbe118fa193c4ccddb9ac0cd9cafe202.json create mode 100644 services/rave-server/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json create mode 100644 services/rave-server/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json create mode 100644 services/rave-server/.sqlx/query-fd5bbe94ae688cbba67694013649ea3160d5e13c6c23bbbfdc7f3f32668b225f.json create mode 100644 services/rave-server/Cargo.toml create mode 100644 services/rave-server/Makefile create mode 100644 services/rave-server/build.rs create mode 100644 services/rave-server/db/migrations/20230705182146_init.sql create mode 100644 services/rave-server/package.json create mode 100755 services/rave-server/script/build create mode 100755 services/rave-server/script/ci create mode 100755 services/rave-server/script/lint create mode 100644 services/rave-server/src/app.rs create mode 100644 services/rave-server/src/config.rs create mode 100644 services/rave-server/src/db.rs create mode 100644 services/rave-server/src/log.rs create mode 100644 services/rave-server/src/main.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index df9825e93a..dfcc905080 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,8 +21,8 @@ executors: password: $VX_DOCKER_PASSWORD jobs: - # @votingworks/admin-backend - test-apps-admin-backend: + # @votingworks/rave-jx-terminal-backend + test-apps-rave-jx-terminal-backend: executor: nodejs resource_class: xlarge steps: @@ -30,22 +30,22 @@ jobs: - run: name: Build command: | - pnpm --dir apps/admin/backend build + pnpm --dir apps/rave-jx-terminal/backend build - run: name: Lint command: | - pnpm --dir apps/admin/backend lint + pnpm --dir apps/rave-jx-terminal/backend lint - run: name: Test command: | - pnpm --dir apps/admin/backend test + pnpm --dir apps/rave-jx-terminal/backend test environment: JEST_JUNIT_OUTPUT_DIR: ./reports/ - store_test_results: - path: apps/admin/backend/reports/ + path: apps/rave-jx-terminal/backend/reports/ - # @votingworks/admin-frontend - test-apps-admin-frontend: + # @votingworks/rave-jx-terminal-frontend + test-apps-rave-jx-terminal-frontend: executor: nodejs resource_class: xlarge steps: @@ -53,323 +53,19 @@ jobs: - run: name: Build command: | - pnpm --dir apps/admin/frontend build + pnpm --dir apps/rave-jx-terminal/frontend build - run: name: Lint command: | - pnpm --dir apps/admin/frontend lint + pnpm --dir apps/rave-jx-terminal/frontend lint - run: name: Test command: | - pnpm --dir apps/admin/frontend test + pnpm --dir apps/rave-jx-terminal/frontend test environment: JEST_JUNIT_OUTPUT_DIR: ./reports/ - store_test_results: - path: apps/admin/frontend/reports/ - - # @votingworks/admin-integration-testing - test-apps-admin-integration-testing: - executor: nodejs-browsers - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Install Browser - command: | - pnpm --dir apps/admin/integration-testing exec playwright install-deps - pnpm --dir apps/admin/integration-testing exec playwright install chromium - - run: - name: Build - command: | - pnpm --dir apps/admin/integration-testing build - - run: - name: Lint - command: | - pnpm --dir apps/admin/integration-testing lint - - run: - name: Test - command: | - pnpm --dir apps/admin/integration-testing test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/admin/integration-testing/reports/ - - store_artifacts: - path: apps/admin/integration-testing/test-results/ - - # @votingworks/central-scan-backend - test-apps-central-scan-backend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/central-scan/backend build - - run: - name: Lint - command: | - pnpm --dir apps/central-scan/backend lint - - run: - name: Test - command: | - pnpm --dir apps/central-scan/backend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/central-scan/backend/reports/ - - # @votingworks/central-scan-frontend - test-apps-central-scan-frontend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/central-scan/frontend build - - run: - name: Lint - command: | - pnpm --dir apps/central-scan/frontend lint - - run: - name: Test - command: | - pnpm --dir apps/central-scan/frontend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/central-scan/frontend/reports/ - - # @votingworks/central-scan-integration-testing - test-apps-central-scan-integration-testing: - executor: nodejs-browsers - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Install Browser - command: | - pnpm --dir apps/central-scan/integration-testing exec playwright install-deps - pnpm --dir apps/central-scan/integration-testing exec playwright install chromium - - run: - name: Build - command: | - pnpm --dir apps/central-scan/integration-testing build - - run: - name: Lint - command: | - pnpm --dir apps/central-scan/integration-testing lint - - run: - name: Test - command: | - pnpm --dir apps/central-scan/integration-testing test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/central-scan/integration-testing/reports/ - - store_artifacts: - path: apps/central-scan/integration-testing/test-results/ - - # @votingworks/design-backend - test-apps-design-backend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/design/backend build - - run: - name: Lint - command: | - pnpm --dir apps/design/backend lint - - run: - name: Test - command: | - pnpm --dir apps/design/backend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/design/backend/reports/ - - # @votingworks/design-frontend - test-apps-design-frontend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/design/frontend build - - run: - name: Lint - command: | - pnpm --dir apps/design/frontend lint - - run: - name: Test - command: | - pnpm --dir apps/design/frontend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/design/frontend/reports/ - - # @votingworks/mark-scan-backend - test-apps-mark-scan-backend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/mark-scan/backend build - - run: - name: Lint - command: | - pnpm --dir apps/mark-scan/backend lint - - run: - name: Test - command: | - pnpm --dir apps/mark-scan/backend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/mark-scan/backend/reports/ - - # @votingworks/mark-scan-frontend - test-apps-mark-scan-frontend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/mark-scan/frontend build - - run: - name: Lint - command: | - pnpm --dir apps/mark-scan/frontend lint - - run: - name: Test - command: | - pnpm --dir apps/mark-scan/frontend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/mark-scan/frontend/reports/ - - # @votingworks/mark-scan-integration-testing - test-apps-mark-scan-integration-testing: - executor: nodejs-browsers - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Install Browser - command: | - pnpm --dir apps/mark-scan/integration-testing exec playwright install-deps - pnpm --dir apps/mark-scan/integration-testing exec playwright install chromium - - run: - name: Build - command: | - pnpm --dir apps/mark-scan/integration-testing build - - run: - name: Lint - command: | - pnpm --dir apps/mark-scan/integration-testing lint - - run: - name: Test - command: | - pnpm --dir apps/mark-scan/integration-testing test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/mark-scan/integration-testing/reports/ - - store_artifacts: - path: apps/mark-scan/integration-testing/test-results/ - - # @votingworks/mark-backend - test-apps-mark-backend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/mark/backend build - - run: - name: Lint - command: | - pnpm --dir apps/mark/backend lint - - run: - name: Test - command: | - pnpm --dir apps/mark/backend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/mark/backend/reports/ - - # @votingworks/mark-frontend - test-apps-mark-frontend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/mark/frontend build - - run: - name: Lint - command: | - pnpm --dir apps/mark/frontend lint - - run: - name: Test - command: | - pnpm --dir apps/mark/frontend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/mark/frontend/reports/ - - # @votingworks/mark-integration-testing - test-apps-mark-integration-testing: - executor: nodejs-browsers - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Install Browser - command: | - pnpm --dir apps/mark/integration-testing exec playwright install-deps - pnpm --dir apps/mark/integration-testing exec playwright install chromium - - run: - name: Build - command: | - pnpm --dir apps/mark/integration-testing build - - run: - name: Lint - command: | - pnpm --dir apps/mark/integration-testing lint - - run: - name: Test - command: | - pnpm --dir apps/mark/integration-testing test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/mark/integration-testing/reports/ - - store_artifacts: - path: apps/mark/integration-testing/test-results/ + path: apps/rave-jx-terminal/frontend/reports/ # @votingworks/rave-mark-backend test-apps-rave-mark-backend: @@ -417,36 +113,8 @@ jobs: - store_test_results: path: apps/rave-mark/frontend/reports/ - # @votingworks/rave-mark-integration-testing - test-apps-rave-mark-integration-testing: - executor: nodejs-browsers - resource_class: xlarge - steps: - - install-cypress-browser - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir apps/rave-mark/integration-testing build - - run: - name: Lint - command: | - pnpm --dir apps/rave-mark/integration-testing lint - - run: - name: Test - command: | - pnpm --dir apps/rave-mark/integration-testing test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: apps/rave-mark/integration-testing/reports/ - - store_artifacts: - path: apps/rave-mark/integration-testing/cypress/screenshots/ - - store_artifacts: - path: apps/rave-mark/integration-testing/cypress/videos/ - - # @votingworks/scan-backend - test-apps-scan-backend: + # @votingworks/rave-scan-backend + test-apps-rave-scan-backend: executor: nodejs resource_class: xlarge steps: @@ -454,22 +122,22 @@ jobs: - run: name: Build command: | - pnpm --dir apps/scan/backend build + pnpm --dir apps/rave-scan/backend build - run: name: Lint command: | - pnpm --dir apps/scan/backend lint + pnpm --dir apps/rave-scan/backend lint - run: name: Test command: | - pnpm --dir apps/scan/backend test + pnpm --dir apps/rave-scan/backend test environment: JEST_JUNIT_OUTPUT_DIR: ./reports/ - store_test_results: - path: apps/scan/backend/reports/ + path: apps/rave-scan/backend/reports/ - # @votingworks/scan-frontend - test-apps-scan-frontend: + # @votingworks/rave-scan-frontend + test-apps-rave-scan-frontend: executor: nodejs resource_class: xlarge steps: @@ -477,19 +145,19 @@ jobs: - run: name: Build command: | - pnpm --dir apps/scan/frontend build + pnpm --dir apps/rave-scan/frontend build - run: name: Lint command: | - pnpm --dir apps/scan/frontend lint + pnpm --dir apps/rave-scan/frontend lint - run: name: Test command: | - pnpm --dir apps/scan/frontend test + pnpm --dir apps/rave-scan/frontend test environment: JEST_JUNIT_OUTPUT_DIR: ./reports/ - store_test_results: - path: apps/scan/frontend/reports/ + path: apps/rave-scan/frontend/reports/ # @votingworks/exercises test-docs-exercises: @@ -514,29 +182,6 @@ jobs: - store_test_results: path: docs/exercises/reports/ - # @votingworks/api - test-libs-api: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/api build - - run: - name: Lint - command: | - pnpm --dir libs/api lint - - run: - name: Test - command: | - pnpm --dir libs/api test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/api/reports/ - # @votingworks/auth test-libs-auth: executor: nodejs @@ -606,8 +251,8 @@ jobs: - store_test_results: path: libs/ballot-encoder/reports/ - # @votingworks/ballot-interpreter - test-libs-ballot-interpreter: + # ballot-encoder-rs + test-libs-ballot-encoder-rs: executor: nodejs resource_class: xlarge steps: @@ -615,19 +260,19 @@ jobs: - run: name: Build command: | - pnpm --dir libs/ballot-interpreter build + pnpm --dir libs/ballot-encoder-rs build - run: name: Lint command: | - pnpm --dir libs/ballot-interpreter lint + pnpm --dir libs/ballot-encoder-rs lint - run: name: Test command: | - pnpm --dir libs/ballot-interpreter test + pnpm --dir libs/ballot-encoder-rs test environment: JEST_JUNIT_OUTPUT_DIR: ./reports/ - store_test_results: - path: libs/ballot-interpreter/reports/ + path: libs/ballot-encoder-rs/reports/ # @votingworks/basics test-libs-basics: @@ -675,77 +320,8 @@ jobs: - store_test_results: path: libs/cdf-schema-builder/reports/ - # @votingworks/converter-nh-accuvote - test-libs-converter-nh-accuvote: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/converter-nh-accuvote build - - run: - name: Lint - command: | - pnpm --dir libs/converter-nh-accuvote lint - - run: - name: Test - command: | - pnpm --dir libs/converter-nh-accuvote test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/converter-nh-accuvote/reports/ - - # @votingworks/custom-paper-handler - test-libs-custom-paper-handler: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/custom-paper-handler build - - run: - name: Lint - command: | - pnpm --dir libs/custom-paper-handler lint - - run: - name: Test - command: | - pnpm --dir libs/custom-paper-handler test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/custom-paper-handler/reports/ - - # @votingworks/custom-scanner - test-libs-custom-scanner: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/custom-scanner build - - run: - name: Lint - command: | - pnpm --dir libs/custom-scanner lint - - run: - name: Test - command: | - pnpm --dir libs/custom-scanner test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/custom-scanner/reports/ - - # @votingworks/cvr-fixture-generator - test-libs-cvr-fixture-generator: + # central-scanner + test-libs-central-scanner: executor: nodejs resource_class: xlarge steps: @@ -753,19 +329,19 @@ jobs: - run: name: Build command: | - pnpm --dir libs/cvr-fixture-generator build + pnpm --dir libs/central-scanner build - run: name: Lint command: | - pnpm --dir libs/cvr-fixture-generator lint + pnpm --dir libs/central-scanner lint - run: name: Test command: | - pnpm --dir libs/cvr-fixture-generator test + pnpm --dir libs/central-scanner test environment: JEST_JUNIT_OUTPUT_DIR: ./reports/ - store_test_results: - path: libs/cvr-fixture-generator/reports/ + path: libs/central-scanner/reports/ # @votingworks/db test-libs-db: @@ -951,52 +527,6 @@ jobs: - store_test_results: path: libs/grout/test-utils/reports/ - # @votingworks/hmpb-layout - test-libs-hmpb-layout: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/hmpb/layout build - - run: - name: Lint - command: | - pnpm --dir libs/hmpb/layout lint - - run: - name: Test - command: | - pnpm --dir libs/hmpb/layout test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/hmpb/layout/reports/ - - # @votingworks/hmpb-render-backend - test-libs-hmpb-render-backend: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/hmpb/render-backend build - - run: - name: Lint - command: | - pnpm --dir libs/hmpb/render-backend lint - - run: - name: Test - command: | - pnpm --dir libs/hmpb/render-backend test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/hmpb/render-backend/reports/ - # @votingworks/image-utils test-libs-image-utils: executor: nodejs @@ -1066,29 +596,6 @@ jobs: - store_test_results: path: libs/mark-flow-ui/reports/ - # @votingworks/message-coder - test-libs-message-coder: - executor: nodejs - resource_class: xlarge - steps: - - checkout-and-install - - run: - name: Build - command: | - pnpm --dir libs/message-coder build - - run: - name: Lint - command: | - pnpm --dir libs/message-coder lint - - run: - name: Test - command: | - pnpm --dir libs/message-coder test - environment: - JEST_JUNIT_OUTPUT_DIR: ./reports/ - - store_test_results: - path: libs/message-coder/reports/ - # @votingworks/monorepo-utils test-libs-monorepo-utils: executor: nodejs @@ -1250,6 +757,29 @@ jobs: - store_test_results: path: libs/ui/reports/ + # @votingworks/ui-rs + test-libs-ui-rs: + executor: nodejs + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + pnpm --dir libs/ui-rs build + - run: + name: Lint + command: | + pnpm --dir libs/ui-rs lint + - run: + name: Test + command: | + pnpm --dir libs/ui-rs test + environment: + JEST_JUNIT_OUTPUT_DIR: ./reports/ + - store_test_results: + path: libs/ui-rs/reports/ + # @votingworks/usb-drive test-libs-usb-drive: executor: nodejs @@ -1296,6 +826,57 @@ jobs: - store_test_results: path: libs/utils/reports/ + # @votingworks/rave-server + test-services-rave-server: + executor: nodejs + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + pnpm --dir services/rave-server build + - run: + name: Lint + command: | + pnpm --dir services/rave-server lint + - run: + name: Test + command: | + pnpm --dir services/rave-server test + environment: + JEST_JUNIT_OUTPUT_DIR: ./reports/ + - store_test_results: + path: services/rave-server/reports/ + + test-crate-ballot-encoder-rs: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p ballot-encoder-rs + - run: + name: Test + command: | + cargo test -p ballot-encoder-rs + + test-crate-central-scanner: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p central-scanner + - run: + name: Test + command: | + cargo test -p central-scanner + test-crate-controllerd: executor: 'nodejs' resource_class: xlarge @@ -1310,6 +891,76 @@ jobs: command: | cargo test -p controllerd + test-crate-rave-jx-terminal-backend: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p rave-jx-terminal-backend + - run: + name: Test + command: | + cargo test -p rave-jx-terminal-backend + + test-crate-rave-jx-terminal-frontend: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p rave-jx-terminal-frontend + - run: + name: Test + command: | + cargo test -p rave-jx-terminal-frontend + + test-crate-rave-scan-backend: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p rave-scan-backend + - run: + name: Test + command: | + cargo test -p rave-scan-backend + + test-crate-rave-scan-frontend: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p rave-scan-frontend + - run: + name: Test + command: | + cargo test -p rave-scan-frontend + + test-crate-rave-server: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p rave-server + - run: + name: Test + command: | + cargo test -p rave-server + test-crate-types-rs: executor: 'nodejs' resource_class: xlarge @@ -1324,6 +975,20 @@ jobs: command: | cargo test -p types-rs + test-crate-ui-rs: + executor: 'nodejs' + resource_class: xlarge + steps: + - checkout-and-install + - run: + name: Build + command: | + cargo build -p ui-rs + - run: + name: Test + command: | + cargo test -p ui-rs + test-crate-vx-logging: executor: 'nodejs' resource_class: xlarge @@ -1355,37 +1020,20 @@ jobs: workflows: test: jobs: - - test-apps-admin-backend - - test-apps-admin-frontend - - test-apps-admin-integration-testing - - test-apps-central-scan-backend - - test-apps-central-scan-frontend - - test-apps-central-scan-integration-testing - - test-apps-design-backend - - test-apps-design-frontend - - test-apps-mark-scan-backend - - test-apps-mark-scan-frontend - - test-apps-mark-scan-integration-testing - - test-apps-mark-backend - - test-apps-mark-frontend - - test-apps-mark-integration-testing + - test-apps-rave-jx-terminal-backend + - test-apps-rave-jx-terminal-frontend - test-apps-rave-mark-backend - test-apps-rave-mark-frontend - - test-apps-rave-mark-integration-testing - - test-apps-scan-backend - - test-apps-scan-frontend + - test-apps-rave-scan-backend + - test-apps-rave-scan-frontend - test-docs-exercises - - test-libs-api - test-libs-auth - test-libs-backend - test-libs-ballot-encoder - - test-libs-ballot-interpreter + - test-libs-ballot-encoder-rs - test-libs-basics - test-libs-cdf-schema-builder - - test-libs-converter-nh-accuvote - - test-libs-custom-paper-handler - - test-libs-custom-scanner - - test-libs-cvr-fixture-generator + - test-libs-central-scanner - test-libs-db - test-libs-dev-dock-backend - test-libs-dev-dock-frontend @@ -1394,12 +1042,9 @@ workflows: - test-libs-fs - test-libs-grout - test-libs-grout-test-utils - - test-libs-hmpb-layout - - test-libs-hmpb-render-backend - test-libs-image-utils - test-libs-logging - test-libs-mark-flow-ui - - test-libs-message-coder - test-libs-monorepo-utils - test-libs-printing - test-libs-res-to-ts @@ -1407,10 +1052,20 @@ workflows: - test-libs-types - test-libs-types-rs - test-libs-ui + - test-libs-ui-rs - test-libs-usb-drive - test-libs-utils + - test-services-rave-server + - test-crate-ballot-encoder-rs + - test-crate-central-scanner - test-crate-controllerd + - test-crate-rave-jx-terminal-backend + - test-crate-rave-jx-terminal-frontend + - test-crate-rave-scan-backend + - test-crate-rave-scan-frontend + - test-crate-rave-server - test-crate-types-rs + - test-crate-ui-rs - test-crate-vx-logging - validate-monorepo diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 8b5473456b..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,15 +0,0 @@ -# VxSuite Code Owners - -# These owners are used to automate code reviews, if there is a specific part of the codebase you would like to be tagged on reviews for add yourself as a code owner. - -# The order of this file is important. The last matching rule will take precedence. - -libs/ballot-encoder/ @eventualbuddha -libs/ballot-interpreter/ @eventualbuddha -libs/eslint-plugin-vx/ @eventualbuddha -libs/logging/ @carolinemodic -.github/ @carolinemodic -libs/auth/ @arsalansufi -libs/dev-dock/ @jonahkagan -libs/grout/ @jonahkagan -libs/usb-drive/ @adghayes diff --git a/.gitignore b/.gitignore index 83c064bbf9..69a5fa742f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,9 @@ node_modules .pnp.js # production -build -dist -target +build/ +target/ +dist/ *-*.*.*.tgz *.tsbuildinfo diff --git a/Cargo.lock b/Cargo.lock index 9299d5c6d7..d68e1bd471 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,13 @@ version = 3 [[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" +name = "addr2line" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] [[package]] name = "adler" @@ -16,11 +19,23 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if 1.0.0", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -35,6 +50,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.5" @@ -84,23 +120,114 @@ dependencies = [ ] [[package]] -name = "approx" -version = "0.5.1" +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.2", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.2", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "async-task" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "atoi" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] [[package]] -name = "atty" -version = "0.2.14" +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic-write-file" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "nix 0.27.1", + "rand", ] [[package]] @@ -110,30 +237,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "ballot-interpreter" -version = "0.1.0" +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ - "base64", - "bitter", - "clap", - "hex", - "image", - "imageproc", - "itertools", - "log", - "logging_timer", - "neon", - "pretty_env_logger", - "proptest", - "rayon", - "rqrr", - "rusttype", + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes 1.5.0", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", "serde", "serde_json", - "tempfile", - "thiserror", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes 1.5.0", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "ballot-encoder-rs" +version = "0.1.0" +dependencies = [ + "bitstream-io", + "color-eyre", + "hex", + "nanoid", + "pretty_assertions", "types-rs", - "zbar-rust", ] [[package]] @@ -142,6 +318,31 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64-serde" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba368df5de76a5bea49aaf0cf1b39ccfbbef176924d1ba5db3e4135216cbe3c7" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -157,12 +358,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -174,12 +369,46 @@ name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "bitstream-io" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e445576659fd04a57b44cbd00aa37aaa815ebefa0aa3cb677a6b5e63d883074f" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.1.1", + "async-lock", + "async-task", + "fastrand", + "futures-io", + "futures-lite", + "piper", + "tracing", +] [[package]] -name = "bitter" -version = "0.6.1" +name = "bumpalo" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803ffa563b84251ab8856d83d6b891e8d161e7df73806e10f78b35af05ba1a04" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" @@ -203,6 +432,32 @@ dependencies = [ "iovec", ] +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "central-scanner" +version = "0.1.0" +dependencies = [ + "color-eyre", + "env_logger", + "log", + "serde", + "tokio", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -215,20 +470,45 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + [[package]] name = "clap" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", @@ -236,12 +516,51 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.46", +] + [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -254,6 +573,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constcat" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" + [[package]] name = "controllerd" version = "0.1.0" @@ -270,12 +620,13 @@ dependencies = [ ] [[package]] -name = "conv" -version = "0.3.3" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "custom_derive", + "core-foundation-sys", + "libc", ] [[package]] @@ -285,20 +636,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] -name = "crc16" -version = "0.4.0" +name = "cpufeatures" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] [[package]] -name = "crc32fast" -version = "1.3.2" +name = "crc" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" dependencies = [ - "cfg-if 1.0.0", + "crc-catalog", ] +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + [[package]] name = "crossbeam-deque" version = "0.8.4" @@ -312,36 +678,49 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" dependencies = [ "autocfg", "cfg-if 1.0.0", "crossbeam-utils", - "memoffset", ] [[package]] -name = "crossbeam-utils" -version = "0.8.17" +name = "crossbeam-queue" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" +checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" dependencies = [ "cfg-if 1.0.0", + "crossbeam-utils", ] [[package]] -name = "crunchy" -version = "0.2.2" +name = "crossbeam-utils" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +dependencies = [ + "cfg-if 1.0.0", +] [[package]] -name = "ctrlc" -version = "3.4.2" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctrlc" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" dependencies = [ "nix 0.27.1", "windows-sys 0.52.0", @@ -354,1237 +733,3115 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" [[package]] -name = "either" -version = "1.9.0" +name = "darling" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] [[package]] -name = "enum-ordinalize" -version = "3.1.15" +name = "darling_core" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" dependencies = [ - "num-bigint", - "num-traits", + "fnv", + "ident_case", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.46", ] [[package]] -name = "enum_derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406ac2a8c9eedf8af9ee1489bee9e50029278a6456c740f7454cf8a158abc816" - -[[package]] -name = "env_logger" -version = "0.7.1" +name = "darling_macro" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", + "darling_core", + "quote", + "syn 2.0.46", ] [[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.8" +name = "der" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ - "libc", - "windows-sys 0.52.0", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "exr" -version = "1.71.0" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", + "powerfmt", + "serde", ] [[package]] -name = "fastrand" -version = "2.0.1" +name = "diff" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] -name = "fdeflate" -version = "0.3.1" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "simd-adler32", + "block-buffer", + "const-oid", + "crypto-common", + "subtle", ] [[package]] -name = "flate2" -version = "1.0.28" +name = "dioxus" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "2d9e3b0725e520250bf23213f996d241cca29cea4360a9bf08a44e0033f8e569" dependencies = [ - "crc32fast", - "miniz_oxide", + "dioxus-core", + "dioxus-core-macro", + "dioxus-hooks", + "dioxus-hot-reload", + "dioxus-html", + "dioxus-rsx", ] [[package]] -name = "flume" -version = "0.11.0" +name = "dioxus-core" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "0f33186615b2e90bceab24a195b3cfad4e0b4d91a33ec44a94845876bfb25c13" dependencies = [ - "spin", + "bumpalo", + "futures-channel", + "futures-util", + "longest-increasing-subsequence", + "rustc-hash", + "serde", + "slab", + "smallbox", + "tracing", ] [[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "g2gen" -version = "1.0.1" +name = "dioxus-core-macro" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2c7625b2fc250dd90b63f7887a6bb0f7ec1d714c8278415bea2669ef20820e" +checksum = "21afaccb28587aed0ba98856335912f5ce7052c0aafa74b213829a3b8bfd2345" dependencies = [ - "g2poly", + "constcat", + "dioxus-core", + "dioxus-rsx", + "prettyplease", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.46", ] [[package]] -name = "g2p" -version = "1.0.1" +name = "dioxus-debug-cell" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc36d9bdc3d2da057775a9f4fa7d7b09edab3e0eda7a92cc353358fa63b8519e" -dependencies = [ - "g2gen", - "g2poly", -] +checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2" [[package]] -name = "g2poly" -version = "1.0.1" +name = "dioxus-hooks" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6a86e750338603ea2c14b1c0bfe58cd61f87ca67a0021d9334996024608e12" +checksum = "5bb23ce82df4fb13e9ddaa01d1469f1f32d683dd4636204bd0a0eaf434b65946" +dependencies = [ + "dioxus-core", + "dioxus-debug-cell", + "futures-channel", + "slab", + "thiserror", + "tracing", +] [[package]] -name = "gcc" -version = "0.3.55" +name = "dioxus-hot-reload" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +checksum = "b7d8c9e89e866a6b84b8ad696f0ff2f6f6563d2235eb99acc6952f19e516cc09" +dependencies = [ + "dioxus-core", + "dioxus-html", + "dioxus-rsx", + "interprocess-docfix", + "serde", + "serde_json", +] [[package]] -name = "getrandom" -version = "0.1.16" +name = "dioxus-html" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "828a42a2d70688a2412a8538c8b5a5eceadf68f682f899dc4455a0169db39dfd" dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "async-channel 1.9.0", + "async-trait", + "dioxus-core", + "enumset", + "euclid", + "keyboard-types", + "serde", + "serde-value", + "serde_json", + "serde_repr", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "getrandom" -version = "0.2.11" +name = "dioxus-interpreter-js" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "d9a3115cf9f550a9af88de615c21a15a72dee44230602087dd7b0c5d01f46c37" dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "js-sys", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "gif" -version = "0.12.0" +name = "dioxus-logger" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +checksum = "3d7cbab0b5519060fe9e14b3c21e3f2329b8386cd905618f78c7b929cd00cf54" dependencies = [ - "color_quant", - "weezl", + "log", + "web-sys", ] [[package]] -name = "half" -version = "2.2.1" +name = "dioxus-router" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +checksum = "25785196405afb2a6ef1b76400d06af88fdf1f8d0315779136465afcddea39a6" dependencies = [ - "crunchy", + "anyhow", + "dioxus", + "dioxus-router-macro", + "futures-util", + "gloo", + "gloo-utils", + "js-sys", + "thiserror", + "tracing", + "url", + "urlencoding", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "hashbrown" -version = "0.13.2" +name = "dioxus-router-macro" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +checksum = "89c7bc30b74ce1082cf7d501ce50c5c1f0629b0c35966bca854e3e9a1d8d4f64" dependencies = [ - "ahash", + "proc-macro2", + "quote", + "slab", + "syn 2.0.46", ] [[package]] -name = "hashbrown" -version = "0.14.3" +name = "dioxus-rsx" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "c974133c7c95497a486d587e40449927711430b308134b9cd374b8d35eceafb3" +dependencies = [ + "dioxus-core", + "proc-macro2", + "quote", + "syn 2.0.46", +] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "dioxus-web" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", +checksum = "fafaff75f50035078c2da45441ee61472fd0f335fa15b05170165eaf3479f0df" +dependencies = [ + "async-channel 1.9.0", + "async-trait", + "console_error_panic_hook", + "dioxus-core", + "dioxus-html", + "dioxus-interpreter-js", + "futures-channel", + "futures-util", + "js-sys", + "rustc-hash", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "hex" -version = "0.4.3" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "humantime" -version = "1.3.0" +name = "either" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" dependencies = [ - "quick-error", + "serde", ] [[package]] -name = "image" -version = "0.24.7" +name = "encoding_rs" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-rational", - "num-traits", - "png", - "qoi", - "tiff", + "cfg-if 1.0.0", ] [[package]] -name = "imageproc" -version = "0.23.0" +name = "enum_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406ac2a8c9eedf8af9ee1489bee9e50029278a6456c740f7454cf8a158abc816" + +[[package]] +name = "enumset" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aee993351d466301a29655d628bfc6f5a35a0d062b6160ca0808f425805fd7" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" dependencies = [ - "approx", - "conv", - "image", - "itertools", - "nalgebra", - "num", - "rand 0.7.3", - "rand_distr", - "rayon", - "rusttype", + "enumset_derive", ] [[package]] -name = "indexmap" -version = "2.1.0" +name = "enumset_derive" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" dependencies = [ - "equivalent", - "hashbrown 0.14.3", + "darling", + "proc-macro2", + "quote", + "syn 2.0.46", ] [[package]] -name = "io-kit-sys" -version = "0.4.0" +name = "env_logger" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ - "core-foundation-sys", - "mach2", + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", ] [[package]] -name = "ioctl-sys" -version = "0.5.2" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c4b26352496eaaa8ca7cfa9bd99e93419d3f7983dc6e99c2a35fe9e33504a" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "iovec" -version = "0.1.4" +name = "errno" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", + "windows-sys 0.52.0", ] [[package]] -name = "itertools" -version = "0.10.5" +name = "etcetera" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "either", + "cfg-if 1.0.0", + "home", + "windows-sys 0.48.0", ] [[package]] -name = "itoa" -version = "1.0.10" +name = "euclid" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +dependencies = [ + "num-traits", + "serde", +] [[package]] -name = "jpeg-decoder" -version = "0.3.0" +name = "event-listener" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "218a870470cce1469024e9fb66b901aa983929d81304a1cdb299f28118e550d5" dependencies = [ - "rayon", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "event-listener-strategy" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.2", + "pin-project-lite", +] [[package]] -name = "lebe" -version = "0.5.2" +name = "eyre" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +dependencies = [ + "indenter", + "once_cell", +] [[package]] -name = "libc" -version = "0.2.151" +name = "fastrand" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] -name = "libloading" -version = "0.6.7" +name = "flume" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ - "cfg-if 1.0.0", - "winapi", + "futures-core", + "futures-sink", + "spin 0.9.8", ] [[package]] -name = "libm" -version = "0.2.8" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "libudev" -version = "0.2.0" +name = "foreign-types" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "libc", - "libudev-sys", + "foreign-types-shared", ] [[package]] -name = "libudev" -version = "0.3.0" +name = "foreign-types-shared" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "libc", - "libudev-sys", + "percent-encoding", ] [[package]] -name = "libudev-sys" -version = "0.1.4" +name = "futures-channel" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ - "libc", - "pkg-config", + "futures-core", + "futures-sink", ] [[package]] -name = "linux-raw-sys" -version = "0.4.12" +name = "futures-core" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] -name = "lock_api" -version = "0.4.11" +name = "futures-executor" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ - "autocfg", - "scopeguard", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" - -[[package]] -name = "logging_timer" -version = "1.1.0" +name = "futures-intrusive" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e96f261d684b7089aa576bb74e823241dccd994b27d30fabf1dcb3af284fe9" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ - "log", - "logging_timer_proc_macros", + "futures-core", + "lock_api", + "parking_lot", ] [[package]] -name = "logging_timer_proc_macros" -version = "1.1.0" +name = "futures-io" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9062912d7952c5588cc474795e0b9ee008e7e6781127945b85413d4b99d81" -dependencies = [ - "log", - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] -name = "lru" -version = "0.9.0" +name = "futures-lite" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" dependencies = [ - "hashbrown 0.13.2", + "futures-core", + "pin-project-lite", ] [[package]] -name = "mach2" -version = "0.4.1" +name = "futures-macro" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "libc", + "proc-macro2", + "quote", + "syn 2.0.46", ] [[package]] -name = "matrixmultiply" -version = "0.3.8" +name = "futures-sink" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" -dependencies = [ - "autocfg", - "rawpointer", -] +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] -name = "memchr" -version = "2.6.4" +name = "futures-task" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] -name = "memoffset" -version = "0.9.0" +name = "futures-util" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ - "autocfg", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] -name = "miniz_oxide" -version = "0.7.1" +name = "g2gen" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "fc2c7625b2fc250dd90b63f7887a6bb0f7ec1d714c8278415bea2669ef20820e" dependencies = [ - "adler", - "simd-adler32", + "g2poly", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "nalgebra" -version = "0.30.1" +name = "g2p" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb2d0de08694bed883320212c18ee3008576bfe8c306f4c3c4a58b4876998be" +checksum = "fc36d9bdc3d2da057775a9f4fa7d7b09edab3e0eda7a92cc353358fa63b8519e" dependencies = [ - "approx", - "matrixmultiply", - "num-complex", - "num-rational", - "num-traits", - "simba", - "typenum", + "g2gen", + "g2poly", ] [[package]] -name = "neon" -version = "0.10.1" +name = "g2poly" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e15415261d880aed48122e917a45e87bb82cf0260bb6db48bbab44b7464373" -dependencies = [ - "neon-build", - "neon-macros", - "neon-runtime", - "semver", - "smallvec", -] +checksum = "af6a86e750338603ea2c14b1c0bfe58cd61f87ca67a0021d9334996024608e12" [[package]] -name = "neon-build" -version = "0.10.1" +name = "gcc" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bac98a702e71804af3dacfde41edde4a16076a7bbe889ae61e56e18c5b1c811" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] -name = "neon-macros" -version = "0.10.1" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7288eac8b54af7913c60e0eb0e2a7683020dffa342ab3fd15e28f035ba897cf" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "quote", - "syn 1.0.109", - "syn-mid", + "typenum", + "version_check", ] [[package]] -name = "neon-runtime" -version = "0.10.1" +name = "getrandom" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676720fa8bb32c64c3d9f49c47a47289239ec46b4bdb66d0913cc512cb0daca" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if 1.0.0", - "libloading", - "smallvec", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "nix" -version = "0.10.0" +name = "gimli" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7fd5681d13fda646462cfbd4e5f2051279a89a544d50eb98c365b507246839f" -dependencies = [ - "bitflags 1.3.2", - "bytes", - "cfg-if 0.1.10", - "gcc", - "libc", - "void", -] +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] -name = "nix" -version = "0.26.4" +name = "gloo" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" dependencies = [ - "bitflags 1.3.2", - "cfg-if 1.0.0", - "libc", + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", ] [[package]] -name = "nix" -version = "0.27.1" +name = "gloo-console" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" dependencies = [ - "bitflags 2.4.1", - "cfg-if 1.0.0", - "libc", + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num" -version = "0.4.1" +name = "gloo-dialogs" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num-bigint" -version = "0.4.4" +name = "gloo-events" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" dependencies = [ - "autocfg", - "num-integer", - "num-traits", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num-complex" -version = "0.4.4" +name = "gloo-file" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" dependencies = [ - "num-traits", + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "gloo-history" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" dependencies = [ - "autocfg", - "num-traits", + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num-iter" -version = "0.1.43" +name = "gloo-net" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" dependencies = [ - "autocfg", - "num-integer", - "num-traits", + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "num-rational" -version = "0.4.1" +name = "gloo-render" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" dependencies = [ - "autocfg", - "num-bigint", - "num-integer", - "num-traits", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "gloo-storage" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" dependencies = [ - "autocfg", - "libm", + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "num_enum" -version = "0.7.1" +name = "gloo-timers" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" dependencies = [ - "num_enum_derive", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "num_enum_derive" -version = "0.7.1" +name = "gloo-utils" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.41", + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "once_cell" -version = "1.19.0" +name = "gloo-worker" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console", + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] -name = "owned_ttf_parser" -version = "0.15.2" +name = "h2" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e6affeb1632d6ff6a23d2cd40ffed138e82f1532571a26f527c8a284bb2fbb" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ - "ttf-parser", + "bytes 1.5.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "paste" -version = "1.0.14" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "patinputd" -version = "0.1.0" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ctrlc", - "uinput", - "vx-logging", + "ahash 0.7.7", ] [[package]] -name = "pkg-config" -version = "0.3.27" +name = "hashbrown" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.7", +] [[package]] -name = "png" -version = "0.17.10" +name = "hashbrown" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", + "ahash 0.8.7", + "allocator-api2", ] [[package]] -name = "ppv-lite86" -version = "0.2.17" +name = "hashlink" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.3", +] [[package]] -name = "pretty_env_logger" -version = "0.4.0" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" dependencies = [ - "env_logger", - "log", + "unicode-segmentation", ] [[package]] -name = "proc-macro-crate" -version = "2.0.1" +name = "hermit-abi" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" -dependencies = [ - "toml_datetime", - "toml_edit", -] +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] -name = "proc-macro2" -version = "1.0.70" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" -dependencies = [ - "unicode-ident", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "proptest" -version = "1.4.0" +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.4.1", - "lazy_static", - "num-traits", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", + "hmac", ] [[package]] -name = "qoi" -version = "0.4.1" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "bytemuck", + "digest", ] [[package]] -name = "quick-error" -version = "1.2.3" +name = "hmac-sha256" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" [[package]] -name = "quote" -version = "1.0.33" +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "proc-macro2", + "windows-sys 0.52.0", ] [[package]] -name = "rand" -version = "0.7.3" +name = "http" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", + "bytes 1.5.0", + "fnv", + "itoa", ] [[package]] -name = "rand" -version = "0.8.5" +name = "http-body" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", + "bytes 1.5.0", + "http", + "pin-project-lite", ] [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "http-range-header" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes 1.5.0", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.5.0", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-rational", + "num-traits", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "interprocess-docfix" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f" +dependencies = [ + "blocking", + "cfg-if 1.0.0", + "futures-core", + "futures-io", + "intmap", + "libc", + "once_cell", + "rustc_version", + "spinning", + "thiserror", + "to_method", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" + +[[package]] +name = "io-kit-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "ioctl-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c4b26352496eaaa8ca7cfa9bd99e93419d3f7983dc6e99c2a35fe9e33504a" + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.4.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libudev" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + +[[package]] +name = "lru" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lru" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if 1.0.0", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7fd5681d13fda646462cfbd4e5f2051279a89a544d50eb98c365b507246839f" +dependencies = [ + "bitflags 1.3.2", + "bytes 0.4.12", + "cfg-if 0.1.10", + "gcc", + "libc", + "void", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +dependencies = [ + "bitflags 2.4.1", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "patinputd" +version = "0.1.0" +dependencies = [ + "ctrlc", + "uinput", + "vx-logging", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.46", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" +dependencies = [ + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.1", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax 0.8.2", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rave-jx-terminal-backend" +version = "0.1.0" +dependencies = [ + "async-stream", + "axum", + "base64", + "base64-serde", + "clap", + "color-eyre", + "dotenvy", + "futures-core", + "openssl", + "pretty_assertions", + "proptest", + "regex", + "reqwest", + "serde", + "serde_json", + "sqlx", + "time", + "tokio", + "tokio-stream", + "tower-http", + "tracing", + "tracing-subscriber", + "types-rs", + "uuid", +] + +[[package]] +name = "rave-jx-terminal-frontend" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "chrono-humanize", + "console_error_panic_hook", + "dioxus", + "dioxus-logger", + "dioxus-router", + "dioxus-web", + "getrandom", + "js-sys", + "log", + "reqwest", + "serde", + "serde_json", + "time", + "types-rs", + "ui-rs", + "wasm-bindgen", + "wasm-logger", + "web-sys", +] + +[[package]] +name = "rave-scan-backend" +version = "0.1.0" +dependencies = [ + "async-stream", + "axum", + "ballot-encoder-rs", + "base64", + "central-scanner", + "clap", + "color-eyre", + "dotenvy", + "env_logger", + "futures-core", + "image", + "pretty_assertions", + "rayon", + "reqwest", + "rqrr", + "serde", + "serde_json", + "sqlx", + "time", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "types-rs", + "uuid", +] + +[[package]] +name = "rave-scan-frontend" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "dioxus", + "dioxus-logger", + "dioxus-web", + "log", + "reqwest", + "serde", + "serde_json", + "types-rs", + "ui-rs", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "rave-server" +version = "0.1.0" +dependencies = [ + "axum", + "base64", + "base64-serde", + "clap", + "color-eyre", + "dotenvy", + "pretty_assertions", + "regex", + "reqwest", + "serde", + "serde_json", + "sqlx", + "time", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "types-rs", + "uuid", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes 1.5.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "rqrr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a8b87d1f9f69bb1a6c77e20fd303f9617b2b68dcff87cd9bcbfff2ced4b8a0b" +dependencies = [ + "g2p", + "image", + "lru 0.9.0", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" + +[[package]] +name = "serde" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "serde_json" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serialport" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +dependencies = [ + "bitflags 2.4.1", + "cfg-if 1.0.0", + "core-foundation-sys", + "io-kit-sys", + "libudev 0.3.0", + "mach2", + "nix 0.26.4", + "regex", + "scopeguard", + "unescaper", + "winapi", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "cfg-if 1.0.0", + "cpufeatures", + "digest", ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "sha2" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "getrandom 0.1.16", + "cfg-if 1.0.0", + "cpufeatures", + "digest", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "getrandom 0.2.11", + "digest", + "rand_core", ] [[package]] -name = "rand_distr" -version = "0.2.2" +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sledgehammer_bindgen" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2" +checksum = "c0bc2cf26c12673eee8674b19d56cec04e9b815704c71298eafac61f131f99d7" dependencies = [ - "rand 0.7.3", + "quote", + "syn 2.0.46", ] [[package]] -name = "rand_hc" +name = "sledgehammer_utils" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "5cd16550f1dd7866c7580dbf80c892dc1bef106737eeb850d42c62ec61896059" dependencies = [ - "rand_core 0.5.1", + "lru 0.8.1", + "once_cell", + "rustc-hash", ] [[package]] -name = "rand_xorshift" -version = "0.3.0" +name = "smallbox" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +checksum = "d92359f97e6b417da4328a970cf04a044db104fbd57f7d72cb7ff665bb8806af" + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ - "rand_core 0.6.4", + "libc", + "windows-sys 0.48.0", ] [[package]] -name = "rawpointer" -version = "0.2.1" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "rayon" -version = "1.8.0" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ + "ahash 0.8.7", + "atoi", + "byteorder", + "bytes 1.5.0", + "crc", + "crossbeam-queue", + "dotenvy", "either", - "rayon-core", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", ] [[package]] -name = "rayon-core" -version = "1.12.0" +name = "sqlx-macros" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", ] [[package]] -name = "redox_syscall" -version = "0.4.1" +name = "sqlx-macros-core" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" dependencies = [ - "bitflags 1.3.2", + "atomic-write-file", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", ] [[package]] -name = "regex" -version = "1.10.2" +name = "sqlx-mysql" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ - "aho-corasick", + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "bytes 1.5.0", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", "memchr", - "regex-automata", - "regex-syntax", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "regex-automata" -version = "0.4.3" +name = "sqlx-postgres" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ - "aho-corasick", + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", "memchr", - "regex-syntax", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", ] [[package]] -name = "regex-syntax" -version = "0.8.2" +name = "sqlx-sqlite" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", + "uuid", +] [[package]] -name = "rqrr" -version = "0.6.0" +name = "stringprep" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a8b87d1f9f69bb1a6c77e20fd303f9617b2b68dcff87cd9bcbfff2ced4b8a0b" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ - "g2p", - "image", - "lru", + "finl_unicode", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "rustix" -version = "0.38.28" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "bitflags 2.4.1", - "errno", + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", "libc", - "linux-raw-sys", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "redox_syscall", + "rustix", "windows-sys 0.52.0", ] [[package]] -name = "rusttype" -version = "0.9.3" +name = "termcolor" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff8374aa04134254b7995b63ad3dc41c7f7236f69528b28553da7d72efaa967" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", + "winapi-util", ] [[package]] -name = "rusty-fork" -version = "0.3.0" +name = "thiserror" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", ] [[package]] -name = "ryu" -version = "1.0.16" +name = "time-core" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] -name = "safe_arch" -version = "0.7.1" +name = "time-macros" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ - "bytemuck", + "time-core", ] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "tinyvec" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] [[package]] -name = "semver" -version = "0.9.0" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "semver-parser" -version = "0.7.0" +name = "to_method" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" [[package]] -name = "serde" -version = "1.0.193" +name = "tokio" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ - "serde_derive", + "backtrace", + "bytes 1.5.0", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", ] [[package]] -name = "serde_derive" -version = "1.0.193" +name = "tokio-macros" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.46", ] [[package]] -name = "serde_json" -version = "1.0.108" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "itoa", - "ryu", - "serde", + "native-tls", + "tokio", ] [[package]] -name = "serialport" -version = "4.3.0" +name = "tokio-stream" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ - "bitflags 2.4.1", - "cfg-if 1.0.0", - "core-foundation-sys", - "io-kit-sys", - "libudev 0.3.0", - "mach2", - "nix 0.26.4", - "regex", - "scopeguard", - "unescaper", - "winapi", + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] -name = "simba" -version = "0.7.3" +name = "tokio-util" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3fd720c48c53cace224ae62bef1bbff363a70c68c4802a78b5cc6159618176" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", + "bytes 1.5.0", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", ] [[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "smallvec" -version = "1.11.2" +name = "toml_datetime" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] -name = "spin" -version = "0.9.8" +name = "toml_edit" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "lock_api", + "indexmap", + "toml_datetime", + "winnow", ] [[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" +name = "tower" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "syn" -version = "2.0.41" +name = "tower-http" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "bitflags 2.4.1", + "bytes 1.5.0", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "syn-mid" -version = "0.5.4" +name = "tower-layer" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] -name = "tempfile" -version = "3.8.1" +name = "tower-service" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" -dependencies = [ - "cfg-if 1.0.0", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.48.0", -] +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] -name = "termcolor" -version = "1.4.0" +name = "tracing" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "winapi-util", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "thiserror" -version = "1.0.51" +name = "tracing-attributes" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.46", ] [[package]] -name = "thiserror-impl" -version = "1.0.51" +name = "tracing-core" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.41", + "once_cell", + "valuable", ] [[package]] -name = "tiff" -version = "0.9.0" +name = "tracing-error" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", + "tracing", + "tracing-subscriber", ] [[package]] -name = "toml_datetime" -version = "0.6.3" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] [[package]] -name = "toml_edit" -version = "0.20.2" +name = "tracing-subscriber" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "indexmap", - "toml_datetime", - "winnow", + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] -name = "ttf-parser" -version = "0.15.2" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -1596,9 +3853,38 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" name = "types-rs" version = "0.1.0" dependencies = [ + "base64", + "base64-serde", + "color-eyre", + "hex", + "hmac-sha256", + "lazy_static", + "nanoid", + "pretty_assertions", "proptest", + "regex", "serde", "serde_json", + "sqlx", + "time", + "uuid", +] + +[[package]] +name = "ui-rs" +version = "0.1.0" +dependencies = [ + "base64", + "dioxus", + "dioxus-logger", + "dioxus-web", + "getrandom", + "js-sys", + "log", + "time", + "wasm-bindgen", + "wasm-logger", + "web-sys", ] [[package]] @@ -1640,18 +3926,100 @@ dependencies = [ "thiserror", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1682,10 +4050,13 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] [[package]] name = "wasi" @@ -1694,21 +4065,104 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "weezl" -version = "0.1.7" +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.46", + "wasm-bindgen-shared", +] [[package]] -name = "wide" -version = "0.7.13" +name = "wasm-bindgen-futures" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68938b57b33da363195412cfc5fc37c9ed49aa9cfe2156fde64b8d2c9498242" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ - "bytemuck", - "safe_arch", + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.46", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "winapi" version = "0.3.9" @@ -1740,6 +4194,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1874,49 +4337,51 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.29" +version = "0.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee40e41f93be5d0057f40462588b988db1354f2c1b1442be186b6df88e8d151" +checksum = "97a4882e6b134d6c28953a387571f1acdd3496830d5e36c5e3a1075580ea641c" dependencies = [ "memchr", ] [[package]] -name = "zbar-rust" -version = "0.0.21" +name = "winreg" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08712bd2e1020ab69e475ccd66afa8e508bf93393a262e2ba56c549cd0865eb" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "enum-ordinalize", - "libc", - "pkg-config", + "cfg-if 1.0.0", + "windows-sys 0.48.0", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.46", ] [[package]] -name = "zune-inflate" -version = "0.2.54" +name = "zeroize" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index a4402ee31b..eadace9ca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,38 +1,93 @@ [workspace] resolver = "2" members = [ - "libs/ballot-interpreter", - "libs/logging", - "libs/types-rs", - "apps/mark-scan/accessible-controller", - "apps/mark-scan/pat-device-input", + "apps/mark-scan/accessible-controller", + "apps/mark-scan/pat-device-input", + "apps/rave-jx-terminal/backend", + "apps/rave-jx-terminal/frontend", + "apps/rave-scan/backend", + "apps/rave-scan/frontend", + "libs/ballot-encoder-rs", + "libs/central-scanner", + "libs/logging", + "libs/types-rs", + "libs/ui-rs", + "services/rave-server", ] [workspace.dependencies] +async-stream = "0.3.5" +axum = { version = "0.6.20" } +ballot-encoder-rs = { path = "libs/ballot-encoder-rs" } base64 = "0.21.4" +base64-serde = "0.7.0" +bitstream-io = "1.7.0" bitter = "0.6.1" -clap = { version = "4.0.29", features = ["cargo"] } +central-scanner = { path = "libs/central-scanner" } +clap = { version = "4.3.23", features = ["cargo", "derive", "env"] } +color-eyre = "0.6.2" crc16 = "0.4.0" ctrlc = "3.4.2" +dioxus = { version = "0.4" } +dioxus-logger = "0.4.1" +dioxus-router = "0.4.1" +dioxus-web = { version = "0.4" } +dotenvy = "0.15.7" +env_logger = "0.10.0" +futures-core = "0.3.28" hex = "0.4.3" -image = "0.24.5" +hmac-sha256 = "1.1.7" +image = { version = "0.24.6", default-features = false, features = ["jpeg"] } imageproc = "0.23.0" itertools = "0.10.5" -log = "0.4.17" vx-logging = { path = "libs/logging" } +js-sys = "0.3.64" +lazy_static = "1.4.0" +log = "0.4.19" logging_timer = "1.1.0" +nanoid = "0.4.0" neon = { version = "0.10", default-features = false, features = ["napi-6"] } num_enum = "0.7.1" +openssl = "0.10.56" +pretty_assertions = "1.4.0" pretty_env_logger = "0.4.0" -proptest = "1.0.0" -rayon = "1.5.3" +proptest = "1.2.0" +rayon = "1.7.0" +regex = "1.9.1" +reqwest = { version = "0.11.18", features = ["json"] } rqrr = "0.6.0" rusttype = "0.9.3" -serde = { version = "1.0.150", features = ["derive"] } -serde_json = "1.0.89" +serde = { version = "1.0.175", features = ["derive"] } +serde_json = "1.0.103" serialport = "4.2.2" +sha256 = "1.2.2" tempfile = "3.3.0" thiserror = "1.0.50" +time = { version = "0.3.22", features = ["formatting", "parsing", "serde"] } +tokio = { version = "1.29.1", default-features = false, features = [ + "sync", + "macros", +] } +tokio-stream = "0.1.14" +tower-http = { version = "0.4.3", features = ["fs"] } +tracing = "0.1.37" +tracing-subscriber = "0.3.17" types-rs = { path = "libs/types-rs" } +ui-rs = { path = "libs/ui-rs" } uinput = "0.1.3" +uuid = { version = "1.4.0", features = ["serde", "v4", "js"] } zbar-rust = "0.0.21" + + +[workspace.dependencies.sqlx] +version = "0.7.1" +default-features = false +features = [ + "macros", + "migrate", + "postgres", + "time", + "uuid", + "json", + "runtime-tokio-rustls", +] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..943bd117a8 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: ./services/rave-server/target/release/rave-server diff --git a/README.md b/README.md index a46414071f..93b327ec61 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,42 @@ -# vxsuite +# RAVE -The VotingWorks in-person voting system. +## Setup -## About +1. Set up a Debian 11 machine (Debian 12 or Ubuntu may work). +2. Clone this repo, `cd` to it. +3. Run the setup script: `script/rave-setup`. +4. Install monorepo dependencies: + - `pnpm install` + - `cargo build` -Includes software for a [ballot-marking device (BMD)](./apps/mark/frontend), a -[ballot scanning device (BSD)](./apps/central-scan/frontend), a -[precinct scanner](./apps/scan/frontend), and an -[election manager](./apps/admin/frontend). See https://voting.works for more -information about VotingWorks. +## Structure -## Development & Build +- `apps/rave-mark`: the RAVE Mark voter-facing application +- `apps/rave-scan`: the RAVE Scan application for ballot scanning +- `apps/rave-jx`: the RAVE Jurisdiction application for election management +- `services/rave-server`: the RAVE server providing sync services for the apps -See the [developer documentation](./docs/development.md). +## Development + +The easiest way to run the services in development is with `mprocs` at the +repository root. + +> Tip: stop and restart the services in `mprocs` with `x` and `r` respectively. + +## Release + +Each app/service can be built and run individually. Try `make run` in each +app/service directory. You'll need to specify the configuration either via +environment variables (e.g. `DATABASE_URL`) or command-line flags. + +## Logging + +Logging for everything but `rave-mark` is configured with the `LOG_LEVEL` +environment variable or `--log-level` CLI option. For example: + +```sh +LOG_LEVEL=debug mprocs +``` ## License diff --git a/RustConfig b/RustConfig new file mode 100644 index 0000000000..ecbda6296f --- /dev/null +++ b/RustConfig @@ -0,0 +1,6 @@ +# build-time settings for `rave-server` in Heroku +BUILD_PATH=services/rave-server + +# tell sqlx to use offline mode, i.e. don't check against the database +# and instead use the query metadata checked into the repository +export SQLX_OFFLINE=true diff --git a/app.json b/app.json new file mode 100644 index 0000000000..a42d556596 --- /dev/null +++ b/app.json @@ -0,0 +1,18 @@ +{ + "addons": [ + + ], + "buildpacks": [ + { + "url": "emk/rust" + } + ], + "env": { + }, + "formation": { + }, + "name": "rave", + "scripts": { + }, + "stack": "heroku-22" +} diff --git a/apps/rave-jx-terminal/Makefile b/apps/rave-jx-terminal/Makefile new file mode 100644 index 0000000000..84228e0547 --- /dev/null +++ b/apps/rave-jx-terminal/Makefile @@ -0,0 +1,40 @@ +APP := rave-jx-terminal + +ALL: build + +clean: clean-frontend clean-backend + +clean-frontend: + @echo "🧹 Cleaning frontend…" + @cd frontend && dx clean + +clean-backend: + @echo "🧹 Cleaning backend…" + @cd backend && cargo clean + +build: build-frontend build-backend + +build-frontend: + @echo "🛠️ Building frontend…" + @cd frontend && dx build --release + +build-backend: + @echo "🛠️ Building backend…" + @cd backend && cargo build --release + +dist: build + @echo "📦 Packaging application…" + @rm -rf dist && mkdir dist + @cp -r frontend/dist dist/public + @cp ../../target/release/$(APP)-backend dist/$(APP) + @echo "- \e[34;4mdist/public\e[0m: frontend assets" + @echo "- \e[34;4mdist/$(APP)\e[0m: application binary" + +run: dist + @echo "🚀 Running application in production mode…" + @cd dist && ./$(APP) + +reset-db: + @echo "🗑️ Resetting database…" + @cd backend && cargo sqlx database reset --source db/migrations + @echo "✅ Database reset" \ No newline at end of file diff --git a/apps/rave-jx-terminal/backend/.env b/apps/rave-jx-terminal/backend/.env new file mode 100644 index 0000000000..dcd50dcb54 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres:rave_jx diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json b/apps/rave-jx-terminal/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json new file mode 100644 index 0000000000..fe46f4f18d --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM elections\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-17be83249f634be8bd73445f7f7860d35e10c7964256492604cd3beb21fa00a4.json b/apps/rave-jx-terminal/backend/.sqlx/query-17be83249f634be8bd73445f7f7860d35e10c7964256492604cd3beb21fa00a4.json new file mode 100644 index 0000000000..b3f726679f --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-17be83249f634be8bd73445f7f7860d35e10c7964256492604cd3beb21fa00a4.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO printed_ballots (\n id,\n server_id,\n client_id,\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n registration_id,\n cast_vote_record,\n cast_vote_record_signature,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (client_id, machine_id)\n DO UPDATE SET\n server_id = $2,\n common_access_card_id = $5,\n common_access_card_certificate = $6,\n cast_vote_record = $8,\n cast_vote_record_signature = $9,\n created_at = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea", + "Uuid", + "Bytea", + "Bytea", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "17be83249f634be8bd73445f7f7860d35e10c7964256492604cd3beb21fa00a4" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json b/apps/rave-jx-terminal/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json new file mode 100644 index 0000000000..bee8b9c1f8 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO elections (\n id,\n server_id,\n client_id,\n machine_id,\n election_hash,\n definition\n )\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (machine_id, client_id)\n DO UPDATE SET\n server_id = $2,\n election_hash = $5,\n definition = $6\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-277f42342d8553dca3dbb0f3bfb118734c6cace06acd1e0adc2487d6418b97cb.json b/apps/rave-jx-terminal/backend/.sqlx/query-277f42342d8553dca3dbb0f3bfb118734c6cace06acd1e0adc2487d6418b97cb.json new file mode 100644 index 0000000000..74a2d5da2f --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-277f42342d8553dca3dbb0f3bfb118734c6cace06acd1e0adc2487d6418b97cb.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registrations (\n id,\n client_id,\n machine_id,\n common_access_card_id,\n registration_request_id,\n election_id,\n precinct_id,\n ballot_style_id\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "277f42342d8553dca3dbb0f3bfb118734c6cace06acd1e0adc2487d6418b97cb" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json b/apps/rave-jx-terminal/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json new file mode 100644 index 0000000000..d7f252d0ee --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO scanned_ballots (\n id,\n server_id,\n client_id,\n machine_id,\n election_id,\n cast_vote_record,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (client_id, machine_id)\n DO UPDATE SET\n server_id = $2,\n election_id = $5,\n cast_vote_record = $6,\n created_at = $7\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Uuid", + "Bytea", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json b/apps/rave-jx-terminal/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json new file mode 100644 index 0000000000..5cd9435a81 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registration_requests (\n id,\n server_id,\n client_id,\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ON CONFLICT (client_id, machine_id)\n DO UPDATE SET\n server_id = $2,\n common_access_card_id = $5,\n given_name = $6,\n family_name = $7,\n address_line_1 = $8,\n address_line_2 = $9,\n city = $10,\n state = $11,\n postal_code = $12,\n state_id = $13,\n created_at = $14\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json b/apps/rave-jx-terminal/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json new file mode 100644 index 0000000000..a393f7a7a8 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM registrations\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-3c3386a91882bc4440873ee682889d88a8224fa5ff6b35599125992a72dd6759.json b/apps/rave-jx-terminal/backend/.sqlx/query-3c3386a91882bc4440873ee682889d88a8224fa5ff6b35599125992a72dd6759.json new file mode 100644 index 0000000000..1b310e1ac3 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-3c3386a91882bc4440873ee682889d88a8224fa5ff6b35599125992a72dd6759.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n (SELECT client_id FROM registration_requests WHERE id = registration_request_id) as \"registration_request_id!: ClientId\",\n (SELECT client_id FROM elections WHERE id = election_id) as \"election_id!: ClientId\",\n precinct_id,\n ballot_style_id\n FROM registrations\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "registration_request_id!: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "election_id!: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "precinct_id", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "ballot_style_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + null, + null, + false, + false + ] + }, + "hash": "3c3386a91882bc4440873ee682889d88a8224fa5ff6b35599125992a72dd6759" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-4a9e5cf9a12c0e30462d79706c83dbdcd42acf8e1dd037900b9a017d663e081f.json b/apps/rave-jx-terminal/backend/.sqlx/query-4a9e5cf9a12c0e30462d79706c83dbdcd42acf8e1dd037900b9a017d663e081f.json new file mode 100644 index 0000000000..5195e34aa6 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-4a9e5cf9a12c0e30462d79706c83dbdcd42acf8e1dd037900b9a017d663e081f.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ClientId\",\n server_id as \"server_id: ServerId\",\n election_id as \"election_id: ClientId\",\n cast_vote_record,\n created_at\n FROM scanned_ballots\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "election_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "4a9e5cf9a12c0e30462d79706c83dbdcd42acf8e1dd037900b9a017d663e081f" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-4cd544bf82339d9d2707a1eaebb2df46a7ed16eb7b67bb3f51ed198a655797d9.json b/apps/rave-jx-terminal/backend/.sqlx/query-4cd544bf82339d9d2707a1eaebb2df46a7ed16eb7b67bb3f51ed198a655797d9.json new file mode 100644 index 0000000000..f042eb00d4 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-4cd544bf82339d9d2707a1eaebb2df46a7ed16eb7b67bb3f51ed198a655797d9.json @@ -0,0 +1,74 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: _\",\n server_id as \"server_id: _\",\n client_id as \"client_id: _\",\n machine_id,\n common_access_card_id,\n registration_request_id as \"registration_request_id: _\",\n election_id as \"election_id: _\",\n precinct_id,\n ballot_style_id,\n created_at\n FROM registrations\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: _", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "client_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "registration_request_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "election_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "precinct_id", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "ballot_style_id", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "4cd544bf82339d9d2707a1eaebb2df46a7ed16eb7b67bb3f51ed198a655797d9" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json b/apps/rave-jx-terminal/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json new file mode 100644 index 0000000000..a06d9b4b83 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO scanned_ballots (\n id,\n server_id,\n client_id,\n machine_id,\n election_id,\n cast_vote_record,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Uuid", + "Bytea", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-596419db69532c6f5062ab2c42990df8aeaa8355d9f5713b2c20e7b22ec0ff45.json b/apps/rave-jx-terminal/backend/.sqlx/query-596419db69532c6f5062ab2c42990df8aeaa8355d9f5713b2c20e7b22ec0ff45.json new file mode 100644 index 0000000000..2df691e9c2 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-596419db69532c6f5062ab2c42990df8aeaa8355d9f5713b2c20e7b22ec0ff45.json @@ -0,0 +1,98 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: _\",\n server_id as \"server_id: _\",\n client_id as \"client_id: _\",\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id,\n created_at\n FROM registration_requests\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: _", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "client_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "given_name", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "family_name", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "address_line_1", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "address_line_2", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "city", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "state", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "postal_code", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "state_id", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "596419db69532c6f5062ab2c42990df8aeaa8355d9f5713b2c20e7b22ec0ff45" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json b/apps/rave-jx-terminal/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json new file mode 100644 index 0000000000..34ea88af2c --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM elections\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-61b98014c74924645df5ea236ae85c893461db917c79417fc9ba5773ebd0fb99.json b/apps/rave-jx-terminal/backend/.sqlx/query-61b98014c74924645df5ea236ae85c893461db917c79417fc9ba5773ebd0fb99.json new file mode 100644 index 0000000000..d0d26ee4fb --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-61b98014c74924645df5ea236ae85c893461db917c79417fc9ba5773ebd0fb99.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n (SELECT client_id FROM registrations WHERE id = registration_id) as \"registration_id!: ClientId\",\n cast_vote_record,\n cast_vote_record_signature\n FROM printed_ballots\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_certificate", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "registration_id!: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 6, + "name": "cast_vote_record_signature", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + null, + false, + false + ] + }, + "hash": "61b98014c74924645df5ea236ae85c893461db917c79417fc9ba5773ebd0fb99" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json b/apps/rave-jx-terminal/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json new file mode 100644 index 0000000000..0c15cf56ae --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM elections\n WHERE machine_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json b/apps/rave-jx-terminal/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json new file mode 100644 index 0000000000..842fdd42bf --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM registrations\n WHERE machine_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-8e90840e2a175e86af47134988e8bacbea4174e84ba25dbd53fe0aee023c2079.json b/apps/rave-jx-terminal/backend/.sqlx/query-8e90840e2a175e86af47134988e8bacbea4174e84ba25dbd53fe0aee023c2079.json new file mode 100644 index 0000000000..4a58cdc8c8 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-8e90840e2a175e86af47134988e8bacbea4174e84ba25dbd53fe0aee023c2079.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ClientId\"\n FROM registrations\n WHERE registration_request_id = $1\n AND election_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "8e90840e2a175e86af47134988e8bacbea4174e84ba25dbd53fe0aee023c2079" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json b/apps/rave-jx-terminal/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json new file mode 100644 index 0000000000..3715f74393 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM registration_requests\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json b/apps/rave-jx-terminal/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json new file mode 100644 index 0000000000..c94203352f --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM printed_ballots\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-a9d72c1006b4e9649cd11cfaff5617b2d34544fd063e7f47b3b2eced9c972711.json b/apps/rave-jx-terminal/backend/.sqlx/query-a9d72c1006b4e9649cd11cfaff5617b2d34544fd063e7f47b3b2eced9c972711.json new file mode 100644 index 0000000000..ff656e586c --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-a9d72c1006b4e9649cd11cfaff5617b2d34544fd063e7f47b3b2eced9c972711.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n definition\n FROM elections\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "definition", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "a9d72c1006b4e9649cd11cfaff5617b2d34544fd063e7f47b3b2eced9c972711" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json b/apps/rave-jx-terminal/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json new file mode 100644 index 0000000000..60755a3d9d --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM scanned_ballots\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-c35748d038bb33658ba19143e130ea922ff0358241be760d2d34798e74d562c0.json b/apps/rave-jx-terminal/backend/.sqlx/query-c35748d038bb33658ba19143e130ea922ff0358241be760d2d34798e74d562c0.json new file mode 100644 index 0000000000..2b443d23a0 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-c35748d038bb33658ba19143e130ea922ff0358241be760d2d34798e74d562c0.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: _\",\n server_id as \"server_id: _\",\n client_id as \"client_id: _\",\n machine_id,\n definition,\n election_hash,\n created_at\n FROM elections\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: _", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "client_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "definition", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "election_hash", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "c35748d038bb33658ba19143e130ea922ff0358241be760d2d34798e74d562c0" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-c3ec601aff31a4f3d0222c94ef6f3b6ecd45df1e8231262648a3e27b176b73bb.json b/apps/rave-jx-terminal/backend/.sqlx/query-c3ec601aff31a4f3d0222c94ef6f3b6ecd45df1e8231262648a3e27b176b73bb.json new file mode 100644 index 0000000000..5d4fa80273 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-c3ec601aff31a4f3d0222c94ef6f3b6ecd45df1e8231262648a3e27b176b73bb.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n common_access_card_id\n FROM registration_requests\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "common_access_card_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c3ec601aff31a4f3d0222c94ef6f3b6ecd45df1e8231262648a3e27b176b73bb" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-d4bd52b3e2c3961766a2120fded05ffb1055e49c4ea8ff9b25f41afc1d1c41e0.json b/apps/rave-jx-terminal/backend/.sqlx/query-d4bd52b3e2c3961766a2120fded05ffb1055e49c4ea8ff9b25f41afc1d1c41e0.json new file mode 100644 index 0000000000..c4aa0e0754 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-d4bd52b3e2c3961766a2120fded05ffb1055e49c4ea8ff9b25f41afc1d1c41e0.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n election_id as \"election_id: ClientId\",\n cast_vote_record,\n created_at\n FROM scanned_ballots\n WHERE server_id IS NULL\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "election_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "d4bd52b3e2c3961766a2120fded05ffb1055e49c4ea8ff9b25f41afc1d1c41e0" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json b/apps/rave-jx-terminal/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json new file mode 100644 index 0000000000..f8b496c33e --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM scanned_ballots\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json b/apps/rave-jx-terminal/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json new file mode 100644 index 0000000000..85dd6ad814 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registrations (\n id,\n server_id,\n client_id,\n machine_id,\n common_access_card_id,\n registration_request_id,\n election_id,\n precinct_id,\n ballot_style_id,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (machine_id, client_id)\n DO UPDATE SET\n server_id = $2,\n common_access_card_id = $5,\n registration_request_id = $6,\n election_id = $7,\n precinct_id = $8,\n ballot_style_id = $9,\n created_at = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json b/apps/rave-jx-terminal/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json new file mode 100644 index 0000000000..20bda01bc2 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM registration_requests\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json b/apps/rave-jx-terminal/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json new file mode 100644 index 0000000000..66e9f85dac --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM registrations\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json b/apps/rave-jx-terminal/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json new file mode 100644 index 0000000000..89fc352bfc --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM printed_ballots\n WHERE machine_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-ea42b1235a28ccdf68b6407099b9d576e13ffcd2ef0dcbcc1027ff2cc7cb8818.json b/apps/rave-jx-terminal/backend/.sqlx/query-ea42b1235a28ccdf68b6407099b9d576e13ffcd2ef0dcbcc1027ff2cc7cb8818.json new file mode 100644 index 0000000000..3874156fb4 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-ea42b1235a28ccdf68b6407099b9d576e13ffcd2ef0dcbcc1027ff2cc7cb8818.json @@ -0,0 +1,74 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id,\n server_id,\n registration_id,\n common_access_card_certificate,\n (\n SELECT election_id\n FROM registrations\n WHERE registrations.id = registration_id\n ),\n (\n SELECT precinct_id\n FROM registrations\n WHERE registrations.id = registration_id\n ),\n (\n SELECT ballot_style_id\n FROM registrations\n WHERE registrations.id = registration_id\n ),\n cast_vote_record,\n cast_vote_record_signature,\n created_at\n FROM printed_ballots\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "registration_id", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "common_access_card_certificate", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "election_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "precinct_id", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "ballot_style_id", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 8, + "name": "cast_vote_record_signature", + "type_info": "Bytea" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + null, + null, + null, + false, + false, + false + ] + }, + "hash": "ea42b1235a28ccdf68b6407099b9d576e13ffcd2ef0dcbcc1027ff2cc7cb8818" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json b/apps/rave-jx-terminal/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json new file mode 100644 index 0000000000..d9cb368ec2 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json @@ -0,0 +1,80 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id\n FROM registration_requests\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "given_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "family_name", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "address_line_1", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "address_line_2", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "city", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "state", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "postal_code", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "state_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-ed3276024209217e2f739174f2b961ccba9af1a9ee0c4751c567aecf9b49d4de.json b/apps/rave-jx-terminal/backend/.sqlx/query-ed3276024209217e2f739174f2b961ccba9af1a9ee0c4751c567aecf9b49d4de.json new file mode 100644 index 0000000000..35cda136a7 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-ed3276024209217e2f739174f2b961ccba9af1a9ee0c4751c567aecf9b49d4de.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: _\",\n server_id as \"server_id: _\",\n client_id as \"client_id: _\",\n machine_id,\n definition,\n election_hash,\n created_at\n FROM elections\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: _", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "client_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "definition", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "election_hash", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "ed3276024209217e2f739174f2b961ccba9af1a9ee0c4751c567aecf9b49d4de" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json b/apps/rave-jx-terminal/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json new file mode 100644 index 0000000000..1d129dedb6 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT created_at\n FROM elections\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef" +} diff --git a/apps/rave-jx-terminal/backend/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json b/apps/rave-jx-terminal/backend/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json new file mode 100644 index 0000000000..34d300f855 --- /dev/null +++ b/apps/rave-jx-terminal/backend/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO elections (\n id,\n client_id,\n machine_id,\n election_hash,\n definition\n )\n VALUES ($1, $2, $3, $4, $5)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff" +} diff --git a/apps/rave-jx-terminal/backend/Cargo.toml b/apps/rave-jx-terminal/backend/Cargo.toml new file mode 100644 index 0000000000..63db620efa --- /dev/null +++ b/apps/rave-jx-terminal/backend/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "rave-jx-terminal-backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-stream = { workspace = true } +axum = { workspace = true } +base64 = { workspace = true } +base64-serde = { workspace = true } +clap = { workspace = true } +color-eyre = { workspace = true } +dotenvy = { workspace = true } +futures-core = { workspace = true } +openssl = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-stream = { workspace = true } +tower-http = { workspace = true, features = ["trace"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +types-rs = { workspace = true, features = ["backend"] } +uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +proptest = { workspace = true } diff --git a/apps/rave-jx-terminal/backend/certs/DODJITCEMAILCA_63.cer b/apps/rave-jx-terminal/backend/certs/DODJITCEMAILCA_63.cer new file mode 100644 index 0000000000000000000000000000000000000000..21ad288b8d6c61f28e2410ba876b5d047eb86c7a GIT binary patch literal 1257 zcmXqLVtH!N#C&!EGZP~d5O^B!vT#(9kjp$TbIY4WV3vBx<-o)<7EK0udK~s0&OBoQtW6k&$8J6jgJ+Y{NhAqE6LEh)%M6I@+MoI~UBB(m%l*o9sh*+H?1Ct5Urm8ol}Fy$oZ!9j zW$l93H|C^0`ZOoP{{3-5_Kr2)ch%y|zVa@2IrM*xk@l*#th3SHOw5c7jEkEXmjXj% zp@BRwC}ovdfN7yY^t{;N!BHkRYwhEK??X-#8^bCLOb5yVPg!t zzMi#?uWoOJ;Nf>627DlCen!UsEG)oe*Jh9o;w!Ku7{nQHvvDT0c`&9jF>n9iG=;dXW=%r*9C+g*9=70qBfoTbpn1lTNL!2Fr^?*VK zjZJJE+H8!htn7@8EM*2I22L=(0aKe9F2gd5i%U~eO7hW6jQ0%DD=<$s(1V-7*ro(E zBR{#gKrgR2&p@vzxvUt)ZOBO(n1_K$nUP^(?vW+N-BAbW<|Eyo@l_kCV z7ke((y)RKV|8wJpNxUU{yeI3g@9$cC^Vssmzf${3H_pzF>Rz<@uj>}vd?;WbV8U`uFQ8}DzPp`9T_3#_ zkNGaTJDKZ>;xhIv)sh>2%v7s;tJ83N-r7YWuiQKOvV#1bmzJ`3uj=_I(fwibIi}8a z{zi}UUL0UAY%jbq>3)s!m93xn4*JYHzJC4Bw|AOWE~yuI66|2Zz4ft1RoT(fXj?_S qg4pR3Q<>yHdY;WZ8T)kVZn@)^j&Q8J@$pK=uWO$KrB}Wy{RsfFB&F;C literal 0 HcmV?d00001 diff --git a/apps/rave-jx-terminal/backend/db/migrations/20230705182146_init.sql b/apps/rave-jx-terminal/backend/db/migrations/20230705182146_init.sql new file mode 100644 index 0000000000..3fcc1673b6 --- /dev/null +++ b/apps/rave-jx-terminal/backend/db/migrations/20230705182146_init.sql @@ -0,0 +1,90 @@ +create table elections ( + id uuid primary key, + -- generated on the server + server_id uuid, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_hash varchar(255) not null, + definition bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table registration_requests ( + id uuid primary key, + -- generated on the server + server_id uuid not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + given_name varchar(255) not null, + family_name varchar(255) not null, + address_line_1 varchar(255) not null, + address_line_2 varchar(255), + city varchar(255) not null, + state varchar(16) not null, + postal_code varchar(255) not null, + state_id varchar(255) not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table registrations ( + id uuid primary key, + -- generated on the server + server_id uuid, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + registration_request_id uuid not null references registration_requests(id) on update cascade on delete cascade, + election_id uuid not null references elections(id) on update cascade on delete cascade, + precinct_id varchar(255) not null, + ballot_style_id varchar(255) not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table printed_ballots ( + id uuid primary key, + -- generated on the server + server_id uuid not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + common_access_card_certificate bytea not null, + registration_id uuid not null references registrations(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + cast_vote_record_signature bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table scanned_ballots ( + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid unique not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_id uuid not null references elections(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); diff --git a/apps/rave-jx-terminal/backend/package.json b/apps/rave-jx-terminal/backend/package.json new file mode 100644 index 0000000000..6a21c5409f --- /dev/null +++ b/apps/rave-jx-terminal/backend/package.json @@ -0,0 +1,20 @@ +{ + "name": "@votingworks/rave-jx-terminal-backend", + "version": "1.0.0", + "description": "VotingWorks RAVE Jurisdiction Terminal", + "scripts": { + "build": "script/build", + "lint": "script/lint", + "start": "cargo watch -x run", + "test:ci": "script/ci", + "test:dev": "cargo test", + "test": "is-ci test:ci test:dev" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "devDependencies": { + "is-ci-cli": "2.2.0" + }, + "packageManager": "pnpm@8.1.0" +} diff --git a/apps/rave-jx-terminal/backend/script/build b/apps/rave-jx-terminal/backend/script/build new file mode 100755 index 0000000000..31f6fdd9cb --- /dev/null +++ b/apps/rave-jx-terminal/backend/script/build @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +SQLX_OFFLINE=true cargo build \ No newline at end of file diff --git a/apps/rave-jx-terminal/backend/script/ci b/apps/rave-jx-terminal/backend/script/ci new file mode 100755 index 0000000000..49d36b1895 --- /dev/null +++ b/apps/rave-jx-terminal/backend/script/ci @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +if [ $(id -u) -eq 0 ] && [ "${CI:-}" == true ]; then + service postgresql start + sudo -u postgres createuser --superuser $(whoami) || true + sudo -u postgres createdb $(cat .env | grep DATABASE_URL | cut -d '=' -f 2 - | cut -d ':' -f 2 -) || true +fi + +SQLX_OFFLINE=true cargo test \ No newline at end of file diff --git a/apps/rave-jx-terminal/backend/script/lint b/apps/rave-jx-terminal/backend/script/lint new file mode 100755 index 0000000000..ee15136feb --- /dev/null +++ b/apps/rave-jx-terminal/backend/script/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +SQLX_OFFLINE=true cargo clippy -- -D warnings \ No newline at end of file diff --git a/apps/rave-jx-terminal/backend/src/app.rs b/apps/rave-jx-terminal/backend/src/app.rs new file mode 100644 index 0000000000..cf506729e5 --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/app.rs @@ -0,0 +1,143 @@ +//! Application definition, including all HTTP route handlers. +//! +//! Route handlers are bundled via [`setup`] into an [`axum::Router`], which can then be run +//! using [`run`] at the configured port (see [`config`][`super::config`]). + +use std::convert::Infallible; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Duration; + +use async_stream::try_stream; +use axum::response::sse::{Event, KeepAlive}; +use axum::response::{Response, Sse}; +use axum::{ + extract::DefaultBodyLimit, + routing::{get, post}, + Json, Router, +}; +use axum::{extract::State, http::StatusCode, response::IntoResponse}; +use futures_core::Stream; +use serde_json::json; +use sqlx::PgPool; +use tokio::time::sleep; +use tower_http::services::{ServeDir, ServeFile}; +use tower_http::trace::TraceLayer; +use tracing::Level; +use types_rs::election::ElectionDefinition; +use types_rs::rave::jx; + +use crate::config::{Config, MAX_REQUEST_SIZE}; +use crate::db::{self, get_app_data}; + +type AppState = (Config, PgPool); + +/// Prepares the application with all the routes. Run the application with +/// `app::run(…)` once you have it. +pub(crate) async fn setup(pool: PgPool, config: Config) -> color_eyre::Result { + let _entered = tracing::span!(Level::DEBUG, "Setting up application").entered(); + + let router = match &config.public_dir { + Some(public_dir) => Router::new().fallback_service( + ServeDir::new(public_dir) + .append_index_html_on_directories(true) + .fallback(ServeFile::new(public_dir.join("index.html"))), + ), + None => { + tracing::info!("No PUBLIC_DIR configured, serving no files"); + Router::new() + } + }; + + Ok(router + .route("/api/status", get(get_status)) + .route("/api/status-stream", get(get_status_stream)) + .route("/api/elections", post(create_election)) + .route("/api/registrations", post(create_registration)) + .layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE)) + .layer(TraceLayer::new_for_http()) + .with_state((config, pool))) +} + +/// Runs an application built by `app::setup(…)`. +pub(crate) async fn run(app: Router, config: &Config) -> color_eyre::Result<()> { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), config.port); + tracing::info!("Server listening at http://{addr}/"); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await?; + Ok(()) +} + +async fn get_status() -> impl IntoResponse { + StatusCode::OK +} + +async fn get_status_stream( + State((_, pool)): State, +) -> Sse>> { + let mut last_app_data = jx::AppData::default(); + + Sse::new(try_stream! { + loop { + let mut connection = pool.acquire().await.unwrap(); + let app_data = get_app_data(&mut connection).await.unwrap(); + + if app_data != last_app_data { + yield Event::default().json_data(&app_data).unwrap(); + last_app_data = app_data; + } + + sleep(Duration::from_secs(1)).await; + } + }) + .keep_alive(KeepAlive::default()) +} + +async fn create_election( + State((config, pool)): State, + election: String, +) -> impl IntoResponse { + let election_definition: ElectionDefinition = election.parse().map_err(into_internal_error)?; + let mut connection = pool.acquire().await.map_err(into_internal_error)?; + + db::add_election(&mut connection, &config, election_definition) + .await + .map_err(into_internal_error)?; + + Ok::<_, Response>(StatusCode::CREATED) +} + +async fn create_registration( + State((config, pool)): State, + registration: Json, +) -> impl IntoResponse { + let ballot_style_id = ®istration.ballot_style_id; + let precinct_id = ®istration.precinct_id; + + let mut connection = pool.acquire().await.map_err(into_internal_error)?; + + db::create_registration( + &mut connection, + &config, + registration.registration_request_id, + registration.election_id, + precinct_id, + ballot_style_id, + ) + .await + .map_err(into_internal_error)?; + + Ok::<_, Response>(StatusCode::CREATED) +} + +fn into_internal_error(e: impl std::fmt::Display) -> Response { + tracing::error!("internal error: {e}"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "success": false, + "error": format!("{e}") + })), + ) + .into_response() +} diff --git a/apps/rave-jx-terminal/backend/src/cac.rs b/apps/rave-jx-terminal/backend/src/cac.rs new file mode 100644 index 0000000000..fdb0e66425 --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/cac.rs @@ -0,0 +1,247 @@ +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Public}; +use openssl::sign::Verifier; +use openssl::x509::X509; +use types_rs::rave::jx::VerificationStatus; + +const DOD_TEST_CAC_CERTIFICATE_BYTES: &[u8] = include_bytes!("../certs/DODJITCEMAILCA_63.cer"); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum CertificateAuthority { + DodTest, +} + +pub(crate) fn verify_cast_vote_record( + common_access_card_certificate: &[u8], + cast_vote_record: &[u8], + cast_vote_record_signature: &[u8], + ca: CertificateAuthority, +) -> VerificationStatus { + let common_access_card_certificate = match X509::from_pem(common_access_card_certificate) { + Ok(x509) => x509, + Err(err) => { + return VerificationStatus::Error(format!( + "error parsing X509 certificate from PEM format: {err}" + )); + } + }; + + match verify_common_access_card_certificate(&common_access_card_certificate, ca) { + Ok(true) => { + // certificate is valid, continue + } + Ok(false) => { + return VerificationStatus::Failure; + } + Err(err) => { + return VerificationStatus::Error(format!( + "error verifying CAC certificate against {ca:?} CA: {err}" + )); + } + } + + let public_key = match common_access_card_certificate.public_key() { + Ok(public_key) => public_key, + Err(err) => { + return VerificationStatus::Error(format!( + "error extracting public key from X509 certificate: {err}" + )); + } + }; + + match verify_signature(cast_vote_record, cast_vote_record_signature, &public_key) { + Ok(true) => { + // signature is valid, continue + } + Ok(false) => { + return VerificationStatus::Failure; + } + Err(err) => { + return VerificationStatus::Error(format!("error verifying signature: {err}")); + } + } + + let Some(CommonNameMetadata { + common_access_card_id, + surname, + given_name, + middle_name, + }) = extract_common_name_metadata(&common_access_card_certificate) else { + return VerificationStatus::Error( + "could not extract and parse CN field from X509 certificate".to_owned() + ); + }; + let display_name = format!("{surname}, {given_name} {middle_name}") + .trim() + .to_owned(); + + VerificationStatus::Success { + common_access_card_id, + display_name, + } +} + +/// Verify that the certificate is signed by the given certificate authority. +/// This function only supports the DOD Test CA. +fn verify_common_access_card_certificate( + common_access_card_certificate: &X509, + ca: CertificateAuthority, +) -> color_eyre::Result { + match ca { + CertificateAuthority::DodTest => { + let signing_cert = match X509::from_der(DOD_TEST_CAC_CERTIFICATE_BYTES) { + Ok(signing_cert) => signing_cert, + Err(err) => { + return Err(color_eyre::eyre::eyre!( + "error parsing {ca:?} certificate from DER format: {err}" + )); + } + }; + + let public_key = match signing_cert.public_key() { + Ok(public_key) => public_key, + Err(err) => { + return Err(color_eyre::eyre::eyre!( + "error extracting public key from {ca:?} certificate: {err}" + )); + } + }; + + // directly verifies the certificate against the DOD Test CA because + // we're only verifying against the one certificate for now. when we + // need to verify the whole chain, we'll need to use the appropriate + // openssl APIs to verify the chain or perform this check in a loop + Ok(common_access_card_certificate.verify(&public_key)?) + } + } +} + +fn verify_signature( + message_buffer: &[u8], + signature_buffer: &[u8], + public_key: &PKey, +) -> color_eyre::Result { + let mut verifier = Verifier::new(MessageDigest::sha256(), public_key)?; + + // Update the verifier with the message data + verifier.update(message_buffer)?; + + // Verify the signature + Ok(verifier.verify(signature_buffer)?) +} + +#[derive(Debug, PartialEq)] +struct CommonNameMetadata { + common_access_card_id: String, + surname: String, + given_name: String, + middle_name: String, +} + +fn extract_common_name_metadata(x509: &X509) -> Option { + // extract the common access card id and name from the certificate + let common_name_entry = x509 + .subject_name() + .entries_by_nid(openssl::nid::Nid::COMMONNAME) + .next()?; + + let common_name_value = common_name_entry + .data() + .as_utf8() + .ok() + .map(|s| s.to_string())?; + + // extract the common access card id and name from the certificate into a tuple + // the string is in this format: "SURNAME.FIRSTNAME.MIDDLENAME.ID" + let common_name_parts: Vec<&str> = common_name_value.split('.').collect(); + if common_name_parts.len() != 4 { + return None; + } + + Some(CommonNameMetadata { + common_access_card_id: common_name_parts[3].trim().to_owned(), + surname: common_name_parts[0].trim().to_owned(), + given_name: common_name_parts[1].trim().to_owned(), + middle_name: common_name_parts[2].trim().to_owned(), + }) +} + +#[cfg(test)] +mod test { + use super::*; + use openssl::x509::{X509Builder, X509NameBuilder}; + use pretty_assertions::assert_eq; + use proptest::{prop_assert_eq, proptest}; + + #[test] + fn extract_metadata_from_sample_cac_cert() { + let cert_bytes = include_bytes!("../tests/fixtures/robert_aikins_sample_cert.pem"); + let x509 = X509::from_pem(cert_bytes).unwrap(); + assert_eq!( + extract_common_name_metadata(&x509), + Some(CommonNameMetadata { + common_access_card_id: "1404922102".to_owned(), + surname: "AIKINS".to_owned(), + given_name: "ROBERT".to_owned(), + middle_name: "EDDIE".to_owned(), + }), + ); + } + + proptest! { + #[test] + /// Generate a random X509 certificate and verify that the common name + /// metadata can be extracted from it. The names are limited to 17 + /// characters because the maximum length of the CN field is 64 bytes, + /// the Common Access Card ID is 10 digits, and the separators are 3 + /// characters. 64 - 10 - 3 = 51 available bytes → 51 / 3 = 17 + /// characters each. + fn test_generated_x509( + common_access_card_id in "\\d{10}", + given_name in "[A-Z]{1,17}", + surname in "[A-Z]{1,17}", + middle_name in "[A-Z]{0,17}", + ) { + let mut subject_name = X509NameBuilder::new().unwrap(); + subject_name.append_entry_by_nid( + openssl::nid::Nid::COMMONNAME, + format!("{surname}.{given_name}.{middle_name}.{common_access_card_id}").as_str(), + ).unwrap(); + let mut x509_builder = X509Builder::new().unwrap(); + x509_builder.set_subject_name(&subject_name.build()).unwrap(); + let x509 = x509_builder.build(); + + prop_assert_eq!( + extract_common_name_metadata(&x509), + Some(CommonNameMetadata { + common_access_card_id, + surname, + given_name, + middle_name, + }) + ); + } + + #[test] + /// Generate a random X509 certificate and verify that the common name + /// metadata cannot be extracted from it because the CN field is + /// invalid. + fn test_invalid_common_names( + common_name in "[^\\.]{1,64}", + ) { + let mut subject_name = X509NameBuilder::new().unwrap(); + subject_name.append_entry_by_nid( + openssl::nid::Nid::COMMONNAME, + common_name.as_str(), + ).unwrap(); + let mut x509_builder = X509Builder::new().unwrap(); + x509_builder.set_subject_name(&subject_name.build()).unwrap(); + let x509 = x509_builder.build(); + + prop_assert_eq!( + extract_common_name_metadata(&x509), + None + ); + } + } +} diff --git a/apps/rave-jx-terminal/backend/src/config.rs b/apps/rave-jx-terminal/backend/src/config.rs new file mode 100644 index 0000000000..754c5bc06f --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/config.rs @@ -0,0 +1,38 @@ +//! Application configuration. + +use std::time::Duration; + +use clap::Parser; + +const TEN_MB: usize = 10 * 1024 * 1024; + +pub(crate) const MAX_REQUEST_SIZE: usize = TEN_MB; +pub(crate) const SYNC_INTERVAL: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone, Parser)] +#[command(author, version, about)] +pub(crate) struct Config { + /// URL of the RAVE server, e.g. `https://rave.example.com/`. + #[arg(long, env = "RAVE_URL")] + pub(crate) rave_url: reqwest::Url, + + /// URL of the PostgreSQL database, e.g. `postgres://user:pass@host:port/dbname`. + #[arg(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + /// ID of this machine, e.g. `machine-1`. + #[arg(long, env = "VX_MACHINE_ID")] + pub(crate) machine_id: String, + + /// Port to listen on. + #[arg(long, env = "PORT")] + pub(crate) port: u16, + + /// Directory to serve static files from. + #[arg(long, env = "PUBLIC_DIR")] + pub(crate) public_dir: Option, + + /// Log level. + #[arg(long, env = "LOG_LEVEL", default_value = "info")] + pub(crate) log_level: tracing::Level, +} diff --git a/apps/rave-jx-terminal/backend/src/db.rs b/apps/rave-jx-terminal/backend/src/db.rs new file mode 100644 index 0000000000..97e7a9dd6a --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/db.rs @@ -0,0 +1,1346 @@ +use std::str::FromStr; +use std::time::Duration; + +use base64_serde::base64_serde_type; +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; +use tracing::Level; +use types_rs::cdf::cvr::Cvr; +use types_rs::election::{BallotStyleId, ElectionDefinition, ElectionHash, PrecinctId}; +use types_rs::rave::jx; +use types_rs::rave::{client, ClientId, ServerId}; +use uuid::Uuid; + +use crate::cac::{verify_cast_vote_record, CertificateAuthority}; +use crate::config::Config; + +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +/// Sets up the database pool and runs any pending migrations, returning the +/// pool to be used by the app. +pub(crate) async fn setup(config: &Config) -> color_eyre::Result { + let _entered = tracing::span!(Level::DEBUG, "Setting up database").entered(); + let pool = PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(3)) + .connect(&config.database_url) + .await?; + tracing::debug!("Running database migrations"); + sqlx::migrate!("db/migrations").run(&pool).await?; + Ok(pool) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Election { + pub(crate) id: ClientId, + pub(crate) server_id: Option, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) definition: ElectionDefinition, + pub(crate) election_hash: ElectionHash, + #[serde(with = "time::serde::iso8601")] + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RegistrationRequest { + pub(crate) id: ClientId, + pub(crate) server_id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) common_access_card_id: String, + pub(crate) given_name: String, + pub(crate) family_name: String, + pub(crate) address_line_1: String, + pub(crate) address_line_2: Option, + pub(crate) city: String, + pub(crate) state: String, + pub(crate) postal_code: String, + pub(crate) state_id: String, + #[serde(with = "time::serde::iso8601")] + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Registration { + pub(crate) id: ClientId, + pub(crate) server_id: Option, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) common_access_card_id: String, + pub(crate) registration_request_id: ClientId, + pub(crate) election_id: ClientId, + pub(crate) precinct_id: String, + pub(crate) ballot_style_id: String, + #[serde(with = "time::serde::iso8601")] + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ScannedBallot { + pub(crate) id: ClientId, + pub(crate) server_id: Option, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) election_id: ClientId, + #[serde(with = "Base64Standard")] + pub(crate) cast_vote_record: Vec, + #[serde(with = "time::serde::iso8601")] + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +pub(crate) async fn get_app_data( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result { + let elections = get_elections(executor, None).await?; + let registration_requests = get_registration_requests(executor).await?; + let registrations = get_registrations(executor).await?; + let printed_ballots = get_printed_ballots(executor).await?; + let scanned_ballots = get_scanned_ballots(executor).await?; + + Ok(jx::AppData { + registrations: registrations + .into_iter() + .map(|r| { + let registration_request = registration_requests + .iter() + .find(|rr| rr.id == r.registration_request_id) + .ok_or_else(|| { + color_eyre::eyre::eyre!( + "registration request not found for registration {}", + r.id + ) + })?; + let election = elections + .iter() + .find(|e| e.id == r.election_id) + .ok_or_else(|| { + color_eyre::eyre::eyre!("election not found for registration {}", r.id) + })?; + Ok(jx::Registration::new( + r.id, + r.server_id, + format!( + "{} {}", + registration_request.given_name, registration_request.family_name + ), + registration_request.common_access_card_id.clone(), + r.registration_request_id, + election.definition.election.title.clone(), + election.election_hash.clone(), + PrecinctId::from(r.precinct_id), + BallotStyleId::from(r.ballot_style_id), + r.created_at, + )) + }) + .collect::>>()?, + elections: elections + .into_iter() + .map(|e| { + jx::Election::new( + e.id, + e.server_id, + e.definition.election.title, + e.definition.election.date.date(), + e.definition.election.ballot_styles, + e.definition.election_hash, + e.created_at, + ) + }) + .collect(), + registration_requests: registration_requests + .into_iter() + .map(|rr| { + jx::RegistrationRequest::new( + rr.id, + rr.server_id, + rr.common_access_card_id, + format!("{} {}", rr.given_name, rr.family_name), + rr.created_at, + ) + }) + .collect(), + printed_ballots, + scanned_ballots, + }) +} + +pub(crate) async fn get_last_synced_election_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM elections + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_last_synced_registration_request_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM registration_requests + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_last_synced_registration_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM registrations + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_last_synced_printed_ballot_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM printed_ballots + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +#[allow(dead_code)] +pub(crate) async fn get_elections( + executor: &mut sqlx::PgConnection, + since_election_id: Option, +) -> Result, color_eyre::eyre::Error> { + let since_election = match since_election_id { + Some(id) => sqlx::query!( + r#" + SELECT created_at + FROM elections + WHERE id = $1 + "#, + id.as_uuid(), + ) + .fetch_optional(&mut *executor) + .await + .ok(), + None => None, + } + .flatten(); + + struct ElectionRecord { + // TODO: use ServerId and ClientId + id: Uuid, + server_id: Option, + client_id: Uuid, + machine_id: String, + definition: Vec, + election_hash: String, + created_at: sqlx::types::time::OffsetDateTime, + } + + let records = match since_election { + Some(election) => { + sqlx::query_as!( + ElectionRecord, + r#" + SELECT + id as "id: _", + server_id as "server_id: _", + client_id as "client_id: _", + machine_id, + definition, + election_hash, + created_at + FROM elections + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + election.created_at + ) + .fetch_all(&mut *executor) + .await? + } + None => { + sqlx::query_as!( + ElectionRecord, + r#" + SELECT + id as "id: _", + server_id as "server_id: _", + client_id as "client_id: _", + machine_id, + definition, + election_hash, + created_at + FROM elections + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await? + } + }; + + records + .into_iter() + .map(|record| { + Ok(Election { + id: record.id.into(), + server_id: record.server_id.map(Into::into), + client_id: record.client_id.into(), + machine_id: record.machine_id, + definition: String::from_utf8(record.definition)?.parse()?, + election_hash: ElectionHash::from_str(record.election_hash.as_str())?, + created_at: record.created_at, + }) + }) + .collect::, _>>() +} + +pub(crate) async fn add_election( + executor: &mut sqlx::PgConnection, + config: &Config, + election: ElectionDefinition, +) -> color_eyre::Result { + let client_id = ClientId::new(); + + sqlx::query!( + r#" + INSERT INTO elections ( + id, + client_id, + machine_id, + election_hash, + definition + ) + VALUES ($1, $2, $3, $4, $5) + "#, + client_id.as_uuid(), + client_id.as_uuid(), + config.machine_id.clone(), + election.election_hash.as_str(), + election.election_data, + ) + .execute(&mut *executor) + .await?; + + Ok(client_id) +} + +pub(crate) async fn get_registration_requests( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + sqlx::query_as!( + RegistrationRequest, + r#" + SELECT + id as "id: _", + server_id as "server_id: _", + client_id as "client_id: _", + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at + FROM registration_requests + ORDER BY created_at DESC + "# + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into) +} + +pub(crate) async fn get_registrations( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + sqlx::query_as!( + Registration, + r#" + SELECT + id as "id: _", + server_id as "server_id: _", + client_id as "client_id: _", + machine_id, + common_access_card_id, + registration_request_id as "registration_request_id: _", + election_id as "election_id: _", + precinct_id, + ballot_style_id, + created_at + FROM registrations + ORDER BY created_at DESC + "# + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into) +} + +pub(crate) async fn get_printed_ballots( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + id, + server_id, + registration_id, + common_access_card_certificate, + ( + SELECT election_id + FROM registrations + WHERE registrations.id = registration_id + ), + ( + SELECT precinct_id + FROM registrations + WHERE registrations.id = registration_id + ), + ( + SELECT ballot_style_id + FROM registrations + WHERE registrations.id = registration_id + ), + cast_vote_record, + cast_vote_record_signature, + created_at + FROM printed_ballots + ORDER BY created_at DESC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|record| { + let verification_status = verify_cast_vote_record( + &record.common_access_card_certificate, + &record.cast_vote_record, + &record.cast_vote_record_signature, + CertificateAuthority::DodTest, + ); + + Ok(jx::PrintedBallot { + id: record.id.into(), + server_id: ServerId::from(record.server_id), + registration_id: record.registration_id.into(), + election_id: record.election_id.map(Into::into).ok_or_else(|| { + color_eyre::eyre::eyre!( + "election_id is null for registration_id {}", + record.registration_id + ) + })?, + precinct_id: record.precinct_id.map(PrecinctId::from).ok_or_else(|| { + color_eyre::eyre::eyre!( + "precinct_id is null for registration_id {}", + record.registration_id + ) + })?, + ballot_style_id: record.ballot_style_id.map(BallotStyleId::from).ok_or_else( + || { + color_eyre::eyre::eyre!( + "ballot_style_id is null for registration_id {}", + record.registration_id + ) + }, + )?, + cast_vote_record: record.cast_vote_record, + cast_vote_record_signature: record.cast_vote_record_signature, + verification_status, + created_at: record.created_at, + }) + }) + .collect::, _>>() +} + +pub(crate) async fn get_scanned_ballots( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + id as "id: ClientId", + server_id as "server_id: ServerId", + election_id as "election_id: ClientId", + cast_vote_record, + created_at + FROM scanned_ballots + ORDER BY created_at DESC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|record| { + let cast_vote_record: Cvr = serde_json::from_slice(&record.cast_vote_record)?; + + Ok(jx::ScannedBallot { + id: record.id, + server_id: record.server_id, + election_id: record.election_id, + precinct_id: PrecinctId::from(cast_vote_record.ballot_style_unit_id.unwrap()), + ballot_style_id: BallotStyleId::from(cast_vote_record.ballot_style_id.unwrap()), + cast_vote_record: record.cast_vote_record, + created_at: record.created_at, + }) + }) + .collect::, _>>() +} + +pub(crate) async fn create_registration( + executor: &mut sqlx::PgConnection, + config: &Config, + registration_request_id: ClientId, + election_id: ClientId, + precinct_id: &PrecinctId, + ballot_style_id: &BallotStyleId, +) -> color_eyre::Result { + let common_access_card_id = sqlx::query!( + r#" + SELECT + common_access_card_id + FROM registration_requests + WHERE id = $1 + "#, + registration_request_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .common_access_card_id; + + let registration_id = ClientId::new(); + + sqlx::query!( + r#" + INSERT INTO registrations ( + id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + "#, + registration_id.as_uuid(), + registration_id.as_uuid(), + config.machine_id.clone(), + common_access_card_id, + registration_request_id.as_uuid(), + election_id.as_uuid(), + precinct_id.to_string(), + ballot_style_id.to_string(), + ) + .execute(&mut *executor) + .await?; + + let registration_id = sqlx::query!( + r#" + SELECT + id as "id: ClientId" + FROM registrations + WHERE registration_request_id = $1 + AND election_id = $2 + "#, + registration_request_id.as_uuid(), + election_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(registration_id) +} + +pub(crate) async fn add_election_from_rave_server( + executor: &mut sqlx::PgConnection, + record: client::output::Election, +) -> color_eyre::Result { + sqlx::query!( + r#" + INSERT INTO elections ( + id, + server_id, + client_id, + machine_id, + election_hash, + definition + ) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (machine_id, client_id) + DO UPDATE SET + server_id = $2, + election_hash = $5, + definition = $6 + "#, + ClientId::new().as_uuid(), + record.server_id.as_uuid(), + record.client_id.as_uuid(), + record.machine_id, + record.definition.election_hash.as_str(), + record.definition as _ + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM elections + WHERE machine_id = $1 AND client_id = $2 + "#, + record.machine_id, + record.client_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_registration_from_rave_server( + executor: &mut sqlx::PgConnection, + registration: client::output::Registration, +) -> color_eyre::Result { + let registration_request_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registration_requests + WHERE server_id = $1 + "#, + registration.registration_request_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + let election_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM elections + WHERE server_id = $1 + "#, + registration.election_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + sqlx::query!( + r#" + INSERT INTO registrations ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (machine_id, client_id) + DO UPDATE SET + server_id = $2, + common_access_card_id = $5, + registration_request_id = $6, + election_id = $7, + precinct_id = $8, + ballot_style_id = $9, + created_at = $10 + "#, + ClientId::new().as_uuid(), + registration.server_id.as_uuid(), + registration.client_id.as_uuid(), + registration.machine_id, + registration.common_access_card_id, + registration_request_id.as_uuid(), + election_id.as_uuid(), + registration.precinct_id, + registration.ballot_style_id, + registration.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registrations + WHERE machine_id = $1 AND client_id = $2 + "#, + registration.machine_id, + registration.client_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_printed_ballot_from_rave_server( + executor: &mut sqlx::PgConnection, + printed_ballot: client::output::PrintedBallot, +) -> color_eyre::Result { + let registration_client_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registrations + WHERE server_id = $1 + "#, + printed_ballot.registration_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + sqlx::query!( + r#" + INSERT INTO printed_ballots ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (client_id, machine_id) + DO UPDATE SET + server_id = $2, + common_access_card_id = $5, + common_access_card_certificate = $6, + cast_vote_record = $8, + cast_vote_record_signature = $9, + created_at = $10 + "#, + ClientId::new().as_uuid(), + printed_ballot.server_id.as_uuid(), + printed_ballot.client_id.as_uuid(), + printed_ballot.machine_id, + printed_ballot.common_access_card_id, + printed_ballot.common_access_card_certificate, + registration_client_id.as_uuid(), + printed_ballot.cast_vote_record, + printed_ballot.cast_vote_record_signature, + printed_ballot.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM printed_ballots + WHERE machine_id = $1 AND client_id = $2 + "#, + printed_ballot.machine_id, + printed_ballot.client_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_scanned_ballot_from_rave_server( + executor: &mut sqlx::PgConnection, + scanned_ballot: client::output::ScannedBallot, +) -> color_eyre::Result { + let election_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM elections + WHERE server_id = $1 + "#, + scanned_ballot.election_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + sqlx::query!( + r#" + INSERT INTO scanned_ballots ( + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (client_id, machine_id) + DO UPDATE SET + server_id = $2, + election_id = $5, + cast_vote_record = $6, + created_at = $7 + "#, + ClientId::new().as_uuid(), + scanned_ballot.server_id.as_uuid(), + scanned_ballot.client_id.as_uuid(), + scanned_ballot.machine_id, + election_id.as_uuid(), + scanned_ballot.cast_vote_record, + scanned_ballot.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM scanned_ballots + WHERE server_id = $1 + "#, + scanned_ballot.server_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_registration_request_from_rave_server( + executor: &mut sqlx::PgConnection, + registration_request: client::output::RegistrationRequest, +) -> color_eyre::Result { + sqlx::query!( + r#" + INSERT INTO registration_requests ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ON CONFLICT (client_id, machine_id) + DO UPDATE SET + server_id = $2, + common_access_card_id = $5, + given_name = $6, + family_name = $7, + address_line_1 = $8, + address_line_2 = $9, + city = $10, + state = $11, + postal_code = $12, + state_id = $13, + created_at = $14 + "#, + ClientId::new().as_uuid(), + registration_request.server_id.as_uuid(), + registration_request.client_id.as_uuid(), + registration_request.machine_id, + registration_request.common_access_card_id, + registration_request.given_name, + registration_request.family_name, + registration_request.address_line_1, + registration_request.address_line_2, + registration_request.city, + registration_request.state, + registration_request.postal_code, + registration_request.state_id, + registration_request.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registration_requests + WHERE server_id = $1 + "#, + registration_request.server_id.as_uuid(), + ) + .fetch_one(executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn get_registration_requests_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id + FROM registration_requests + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + Ok(records + .into_iter() + .map(|r| client::input::RegistrationRequest { + client_id: r.client_id, + machine_id: r.machine_id, + common_access_card_id: r.common_access_card_id, + given_name: r.given_name, + family_name: r.family_name, + address_line_1: r.address_line_1, + address_line_2: r.address_line_2, + city: r.city, + state: r.state, + postal_code: r.postal_code, + state_id: r.state_id, + }) + .collect()) +} + +pub(crate) async fn get_elections_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + definition + FROM elections + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|e| { + Ok(client::input::Election { + client_id: e.client_id, + machine_id: e.machine_id, + definition: String::from_utf8(e.definition)?.parse()?, + }) + }) + .collect() +} + +pub(crate) async fn get_registrations_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + (SELECT client_id FROM registration_requests WHERE id = registration_request_id) as "registration_request_id!: ClientId", + (SELECT client_id FROM elections WHERE id = election_id) as "election_id!: ClientId", + precinct_id, + ballot_style_id + FROM registrations + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|r| { + Ok(client::input::Registration { + client_id: r.client_id, + machine_id: r.machine_id, + election_id: r.election_id, + registration_request_id: r.registration_request_id, + common_access_card_id: r.common_access_card_id, + precinct_id: r.precinct_id, + ballot_style_id: r.ballot_style_id, + }) + }) + .collect() +} + +pub(crate) async fn get_printed_ballots_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + common_access_card_certificate, + (SELECT client_id FROM registrations WHERE id = registration_id) as "registration_id!: ClientId", + cast_vote_record, + cast_vote_record_signature + FROM printed_ballots + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|r| { + Ok(client::input::PrintedBallot { + client_id: r.client_id, + machine_id: r.machine_id, + common_access_card_id: r.common_access_card_id, + common_access_card_certificate: r.common_access_card_certificate, + registration_id: r.registration_id, + cast_vote_record: r.cast_vote_record, + cast_vote_record_signature: r.cast_vote_record_signature, + }) + }) + .collect() +} + +pub(crate) async fn get_scanned_ballots_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + election_id as "election_id: ClientId", + cast_vote_record, + created_at + FROM scanned_ballots + WHERE server_id IS NULL + ORDER BY created_at DESC + "# + ) + .fetch_all(&mut *executor) + .await?; + + Ok(records + .into_iter() + .map(|r| client::input::ScannedBallot { + client_id: r.client_id, + machine_id: r.machine_id, + election_id: r.election_id, + cast_vote_record: r.cast_vote_record, + }) + .collect::>()) +} + +#[allow(dead_code)] +pub(crate) async fn add_scanned_ballot( + executor: &mut sqlx::PgConnection, + scanned_ballot: ScannedBallot, +) -> Result<(), sqlx::Error> { + let ScannedBallot { + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at, + } = scanned_ballot; + sqlx::query!( + r#" + INSERT INTO scanned_ballots ( + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + "#, + id.as_uuid(), + server_id.map(|id| id.as_uuid()), + client_id.as_uuid(), + machine_id, + election_id.as_uuid(), + cast_vote_record, + created_at + ) + .execute(executor) + .await?; + + Ok(()) +} + +pub(crate) async fn get_last_synced_scanned_ballot_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM scanned_ballots + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + use time::OffsetDateTime; + use types_rs::cdf::cvr::Cvr; + + fn load_famous_names_election() -> ElectionDefinition { + let election_json = include_str!("../tests/fixtures/electionFamousNames2021.json"); + election_json.parse().unwrap() + } + + fn build_rave_server_registration_request() -> client::output::RegistrationRequest { + client::output::RegistrationRequest { + server_id: ServerId::new(), + client_id: ClientId::new(), + machine_id: "mark-terminal-001".to_owned(), + common_access_card_id: "0000000000".to_owned(), + given_name: "John".to_owned(), + family_name: "Doe".to_owned(), + address_line_1: "123 Main St".to_owned(), + address_line_2: None, + city: "Anytown".to_owned(), + state: "CA".to_owned(), + postal_code: "95959".to_owned(), + state_id: "CA-12345678".to_owned(), + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_election( + election_definition: ElectionDefinition, + ) -> client::output::Election { + client::output::Election { + server_id: ServerId::new(), + client_id: ClientId::new(), + machine_id: "mark-terminal-001".to_owned(), + election_hash: election_definition.election_hash.clone(), + definition: election_definition, + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_registration( + registration_request: &client::output::RegistrationRequest, + election: &client::output::Election, + election_definition: &ElectionDefinition, + ) -> client::output::Registration { + let ballot_style = &election_definition.election.ballot_styles[0]; + + client::output::Registration { + server_id: registration_request.server_id, + client_id: registration_request.client_id, + machine_id: registration_request.machine_id.clone(), + common_access_card_id: registration_request.common_access_card_id.clone(), + registration_request_id: registration_request.server_id, + election_id: election.server_id, + precinct_id: ballot_style.precincts[0].to_string(), + ballot_style_id: ballot_style.id.to_string(), + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_printed_ballot( + registration: &client::output::Registration, + cast_vote_record: Cvr, + ) -> client::output::PrintedBallot { + client::output::PrintedBallot { + server_id: registration.server_id, + client_id: registration.client_id, + machine_id: registration.machine_id.clone(), + common_access_card_id: registration.common_access_card_id.clone(), + common_access_card_certificate: vec![], + registration_id: registration.registration_request_id, + cast_vote_record: serde_json::to_vec(&cast_vote_record).unwrap(), + cast_vote_record_signature: vec![], + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_scanned_ballot( + election: &client::output::Election, + cast_vote_record: Cvr, + ) -> client::output::ScannedBallot { + client::output::ScannedBallot { + server_id: election.server_id, + client_id: election.client_id, + machine_id: election.machine_id.clone(), + cast_vote_record: serde_json::to_vec(&cast_vote_record).unwrap(), + election_id: election.server_id, + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_config() -> Config { + Config { + rave_url: reqwest::Url::parse("http://localhost:8000").unwrap(), + database_url: "postgres:test".to_owned(), + machine_id: "rave-jx-test".to_owned(), + port: 5000, + public_dir: None, + log_level: tracing::Level::INFO, + } + } + + #[sqlx::test(migrations = "db/migrations")] + async fn test_add_election_from_rave_server(pool: sqlx::PgPool) -> sqlx::Result<()> { + let mut db = pool.acquire().await?; + + let election_definition = load_famous_names_election(); + let record = build_rave_server_election(election_definition.clone()); + + let client_id = add_election_from_rave_server(&mut db, record.clone()) + .await + .unwrap(); + + // insert again, should be idempotent + let client_id2 = add_election_from_rave_server(&mut db, record.clone()) + .await + .unwrap(); + + assert_eq!(client_id, client_id2); + + Ok(()) + } + + #[sqlx::test(migrations = "db/migrations")] + async fn test_add_election_to_be_synced(pool: sqlx::PgPool) -> sqlx::Result<()> { + let mut db = pool.acquire().await?; + + let election_definition = load_famous_names_election(); + + let client_id = add_election(&mut db, &build_config(), election_definition.clone()) + .await + .unwrap(); + + let elections = get_elections_to_sync_to_rave_server(&mut db).await.unwrap(); + + assert_eq!(elections.len(), 1); + assert_eq!(elections[0].client_id, client_id); + assert_eq!( + elections[0].definition.election_data, + election_definition.election_data + ); + + Ok(()) + } + + #[sqlx::test(migrations = "db/migrations")] + async fn test_add_everything_to_database(pool: sqlx::PgPool) -> sqlx::Result<()> { + let mut db = pool.acquire().await?; + + let election_definition = load_famous_names_election(); + let registration_request = build_rave_server_registration_request(); + let election = build_rave_server_election(election_definition.clone()); + let registration = + build_rave_server_registration(®istration_request, &election, &election_definition); + let printed_ballot = build_rave_server_printed_ballot(®istration, Cvr::default()); + let scanned_ballot = build_rave_server_scanned_ballot(&election, Cvr::default()); + + add_election_from_rave_server(&mut db, election) + .await + .unwrap(); + + add_or_update_registration_request_from_rave_server(&mut db, registration_request) + .await + .unwrap(); + + add_or_update_registration_from_rave_server(&mut db, registration) + .await + .unwrap(); + + add_or_update_printed_ballot_from_rave_server(&mut db, printed_ballot) + .await + .unwrap(); + + add_or_update_scanned_ballot_from_rave_server(&mut db, scanned_ballot) + .await + .unwrap(); + + Ok(()) + } +} diff --git a/apps/rave-jx-terminal/backend/src/log.rs b/apps/rave-jx-terminal/backend/src/log.rs new file mode 100644 index 0000000000..aaa0f49b84 --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/log.rs @@ -0,0 +1,35 @@ +//! Logging for RAVE JX. +//! +//! RAVE JX uses the `tracing` library for logging. After calling [`setup`], +//! you'll be able to call [`tracing::info!`], [`tracing::span!`], and others to +//! print log messages to `stdout` in a flexible and configurable way. +//! +//! You may use the `RUST_LOG` environment variable to configure logging at +//! runtime (see [`EnvFilter`][`tracing_subscriber::EnvFilter`]). + +use tracing_subscriber::{prelude::*, util::SubscriberInitExt}; + +use crate::config::Config; + +/// Sets up logging for the application. Call this early in the process +/// lifecycle to ensure logs are not silently ignored. +pub(crate) fn setup(config: &Config) -> color_eyre::Result<()> { + color_eyre::install()?; + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive( + format!( + "{}={}", + env!("CARGO_PKG_NAME").replace('-', "_"), + config.log_level + ) + .parse()?, + ) + .from_env_lossy(), + ) + .with(stdout_log) + .init(); + Ok(()) +} diff --git a/apps/rave-jx-terminal/backend/src/main.rs b/apps/rave-jx-terminal/backend/src/main.rs new file mode 100644 index 0000000000..a76efb6230 --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/main.rs @@ -0,0 +1,61 @@ +//! RAVE JX manages elections, voters, and ballots for a jurisdiction. + +#![warn( + clippy::all, + clippy::todo, + clippy::empty_enum, + clippy::enum_glob_use, + clippy::mem_forget, + clippy::unused_self, + clippy::filter_map_next, + clippy::needless_continue, + clippy::needless_borrow, + clippy::match_wildcard_for_single_variants, + clippy::if_let_mutex, + clippy::mismatched_target_os, + clippy::await_holding_lock, + clippy::match_on_vec_items, + clippy::imprecise_flops, + clippy::suboptimal_flops, + clippy::lossy_float_literal, + clippy::rest_pat_in_fully_bound_structs, + clippy::fn_params_excessive_bools, + clippy::exit, + clippy::inefficient_to_string, + clippy::linkedlist, + clippy::macro_use_imports, + clippy::option_option, + clippy::verbose_file_reads, + clippy::unnested_or_patterns, + clippy::str_to_string, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + missing_debug_implementations, + missing_docs +)] +#![deny(unreachable_pub, private_in_public)] +#![allow(elided_lifetimes_in_paths, clippy::type_complexity)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))] +#![cfg_attr(test, allow(clippy::float_cmp))] +#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))] + +use clap::Parser; + +mod app; +mod cac; +mod config; +mod db; +mod log; +mod sync; + +#[tokio::main] +async fn main() -> color_eyre::Result<()> { + dotenvy::dotenv()?; + let config = config::Config::parse(); + log::setup(&config)?; + let pool = db::setup(&config).await?; + sync::sync_periodically(&pool, config.clone()).await; + app::run(app::setup(pool, config.clone()).await?, &config).await +} diff --git a/apps/rave-jx-terminal/backend/src/sync.rs b/apps/rave-jx-terminal/backend/src/sync.rs new file mode 100644 index 0000000000..ee6c696472 --- /dev/null +++ b/apps/rave-jx-terminal/backend/src/sync.rs @@ -0,0 +1,211 @@ +//! RAVE Server synchronization utilities. + +use sqlx::PgPool; +use tokio::time::sleep; +use tracing::Level; +use types_rs::rave::{RaveServerSyncInput, RaveServerSyncOutput}; + +use crate::{ + config::{Config, SYNC_INTERVAL}, + db, +}; + +/// Spawns an async loop that synchronizes with the RAVE Server on a fixed +/// schedule. +pub(crate) async fn sync_periodically(pool: &PgPool, config: Config) { + let mut connection = pool + .acquire() + .await + .expect("failed to acquire database connection"); + + tokio::spawn(async move { + loop { + match sync(&mut connection, &config).await { + Ok(_) => { + tracing::info!("Successfully synced with RAVE Server"); + } + Err(e) => { + tracing::error!("Failed to sync with RAVE Server: {}", e); + } + } + sleep(SYNC_INTERVAL).await; + } + }); +} + +pub(crate) async fn sync( + executor: &mut sqlx::PgConnection, + config: &Config, +) -> color_eyre::eyre::Result<()> { + let span = tracing::span!(Level::DEBUG, "Syncing with RAVE Server"); + let _enter = span.enter(); + + check_status(config.rave_url.join("/api/status")?).await?; + + let sync_input = RaveServerSyncInput { + last_synced_election_id: db::get_last_synced_election_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!("failed to get last synced election ID: {}", e)) + })?, + last_synced_registration_request_id: db::get_last_synced_registration_request_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get last synced registration request ID: {}", + e + )) + })?, + last_synced_registration_id: db::get_last_synced_registration_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!("failed to get last synced registration ID: {}", e)) + })?, + last_synced_scanned_ballot_id: db::get_last_synced_scanned_ballot_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get last synced scanned ballot ID: {}", + e + )) + })?, + last_synced_printed_ballot_id: db::get_last_synced_printed_ballot_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get last synced printed ballot ID: {}", + e + )) + })?, + elections: db::get_elections_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get elections to sync to RAVE Server: {}", + e + )) + })?, + registration_requests: db::get_registration_requests_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get registration requests to sync to RAVE Server: {}", + e + )) + })?, + registrations: db::get_registrations_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get registrations to sync to RAVE Server: {}", + e + )) + })?, + printed_ballots: db::get_printed_ballots_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get printed ballots to sync to RAVE Server: {}", + e + )) + })?, + scanned_ballots: db::get_scanned_ballots_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get scanned ballots to sync to RAVE Server: {}", + e + )) + })?, + }; + + let sync_endpoint = config + .rave_url + .join("/api/sync") + .expect("failed to construct sync URL"); + let sync_output = request(sync_endpoint, &sync_input).await?; + + let RaveServerSyncOutput { + elections, + registration_requests, + registrations, + printed_ballots, + scanned_ballots, + .. + } = sync_output.clone(); + + for election in elections.into_iter() { + let result = db::add_election_from_rave_server(executor, election).await; + + if let Err(e) = result { + tracing::error!("Failed to insert election: {}", e); + } + } + + for registration_request in registration_requests.into_iter() { + let result = + db::add_or_update_registration_request_from_rave_server(executor, registration_request) + .await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update registration request: {}", e); + } + } + + for registration in registrations.into_iter() { + let result = db::add_or_update_registration_from_rave_server(executor, registration).await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update registration: {}", e); + } + } + + for printed_ballot in printed_ballots.into_iter() { + let result = + db::add_or_update_printed_ballot_from_rave_server(executor, printed_ballot).await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update printed ballot: {}", e); + } + } + + for scanned_ballot in scanned_ballots.into_iter() { + let result = + db::add_or_update_scanned_ballot_from_rave_server(executor, scanned_ballot).await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update scanned ballot: {}", e); + } + } + + Ok(()) +} + +pub(crate) async fn check_status(endpoint: reqwest::Url) -> color_eyre::eyre::Result<()> { + let client = reqwest::Client::new(); + client + .get(endpoint.clone()) + .send() + .await? + .error_for_status() + .map_err(|e| { + color_eyre::eyre::eyre!( + "RAVE Server responded with an error (status URL={endpoint}): {e}", + ) + }) + .map(|_| ()) +} + +pub(crate) async fn request( + endpoint: reqwest::Url, + sync_input: &RaveServerSyncInput, +) -> color_eyre::eyre::Result { + let client = reqwest::Client::new(); + Ok(client + .post(endpoint.clone()) + .json(sync_input) + .send() + .await? + .json::() + .await?) +} diff --git a/apps/rave-jx-terminal/backend/tests/fixtures/electionFamousNames2021.json b/apps/rave-jx-terminal/backend/tests/fixtures/electionFamousNames2021.json new file mode 100755 index 0000000000..3e7a46c1fe --- /dev/null +++ b/apps/rave-jx-terminal/backend/tests/fixtures/electionFamousNames2021.json @@ -0,0 +1,324 @@ +{ + "title": "Lincoln Municipal General Election", + "state": "State of Hamilton", + "county": { + "id": "franklin", + "name": "Franklin County" + }, + "date": "2021-06-06T00:00:00-10:00", + "parties": [ + { + "id": "0", + "name": "Democrat", + "fullName": "Democratic Party", + "abbrev": "D" + }, + { + "id": "1", + "name": "Republican", + "fullName": "Republican Party", + "abbrev": "R" + }, + { + "id": "2", + "name": "Liberty", + "fullName": "Liberty Party", + "abbrev": "Li" + }, + { + "id": "3", + "name": "Green", + "fullName": "Green Party", + "abbrev": "G" + } + ], + "contests": [ + { + "id": "mayor", + "districtId": "district-1", + "type": "candidate", + "title": "Mayor", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "sherlock-holmes", + "name": "Sherlock Holmes", + "partyIds": ["0"] + }, + { + "id": "thomas-edison", + "name": "Thomas Edison", + "partyIds": ["1"] + } + ] + }, + { + "id": "controller", + "districtId": "district-1", + "type": "candidate", + "title": "Controller", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "winston-churchill", + "name": "Winston Churchill", + "partyIds": ["0"] + }, + { + "id": "oprah-winfrey", + "name": "Oprah Winfrey", + "partyIds": ["1"] + }, + { + "id": "louis-armstrong", + "name": "Louis Armstrong", + "partyIds": ["3"] + } + ] + }, + { + "id": "attorney", + "districtId": "district-1", + "type": "candidate", + "title": "Attorney", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "john-snow", + "name": "John Snow", + "partyIds": ["1"] + }, + { + "id": "mark-twain", + "name": "Mark Twain", + "partyIds": ["3"] + } + ] + }, + { + "id": "public-works-director", + "districtId": "district-1", + "type": "candidate", + "title": "Public Works Director", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "benjamin-franklin", + "name": "Benjamin Franklin", + "partyIds": ["0"] + }, + { + "id": "robert-downey-jr", + "name": "Robert Downey Jr.", + "partyIds": ["1"] + }, + { + "id": "bill-nye", + "name": "Bill Nye", + "partyIds": ["3"] + } + ] + }, + { + "id": "chief-of-police", + "districtId": "district-1", + "type": "candidate", + "title": "Chief of Police", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "natalie-portman", + "name": "Natalie Portman", + "partyIds": ["0"] + }, + { + "id": "frank-sinatra", + "name": "Frank Sinatra", + "partyIds": ["1"] + }, + { + "id": "andy-warhol", + "name": "Andy Warhol", + "partyIds": ["3"] + }, + { + "id": "alfred-hitchcock", + "name": "Alfred Hitchcock", + "partyIds": ["3"] + } + ] + }, + { + "id": "parks-and-recreation-director", + "districtId": "district-1", + "type": "candidate", + "title": "Parks and Recreation Director", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "charles-darwin", + "name": "Charles Darwin", + "partyIds": ["0"] + }, + { + "id": "stephen-hawking", + "name": "Stephen Hawking", + "partyIds": ["1"] + }, + { + "id": "johan-sebastian-bach", + "name": "Johann Sebastian Bach", + "partyIds": ["0"] + }, + { + "id": "alexander-graham-bell", + "name": "Alexander Graham Bell", + "partyIds": ["1"] + } + ] + }, + { + "id": "board-of-alderman", + "districtId": "district-1", + "type": "candidate", + "title": "Board of Alderman", + "seats": 4, + "allowWriteIns": true, + "candidates": [ + { + "id": "helen-keller", + "name": "Helen Keller", + "partyIds": ["0"] + }, + { + "id": "steve-jobs", + "name": "Steve Jobs", + "partyIds": ["1"] + }, + { + "id": "nikola-tesla", + "name": "Nikola Tesla", + "partyIds": ["0"] + }, + { + "id": "vincent-van-gogh", + "name": "Vincent Van Gogh", + "partyIds": ["1"] + }, + { + "id": "pablo-picasso", + "name": "Pablo Picasso", + "partyIds": ["1"] + }, + { + "id": "wolfgang-amadeus-mozart", + "name": "Wolfgang Amadeus Mozart", + "partyIds": ["2"] + } + ] + }, + { + "id": "city-council", + "districtId": "district-1", + "type": "candidate", + "title": "City Council", + "seats": 4, + "allowWriteIns": true, + "candidates": [ + { + "id": "marie-curie", + "name": "Marie Curie", + "partyIds": ["0"] + }, + { + "id": "indiana-jones", + "name": "Indiana Jones", + "partyIds": ["1"] + }, + { + "id": "mona-lisa", + "name": "Mona Lisa", + "partyIds": ["3"] + }, + { + "id": "jackie-chan", + "name": "Jackie Chan", + "partyIds": ["3"] + }, + { + "id": "tim-allen", + "name": "Tim Allen", + "partyIds": ["2"] + }, + { + "id": "mark-antony", + "name": "Mark Antony", + "partyIds": ["0"] + }, + { + "id": "harriet-tubman", + "name": "Harriet Tubman", + "partyIds": ["1"] + }, + { + "id": "martin-luther-king", + "name": "Dr. Martin Luther King Jr.", + "partyIds": ["0"] + }, + { + "id": "marilyn-monroe", + "name": "Marilyn Monroe", + "partyIds": ["1"] + } + ] + } + ], + "districts": [ + { + "id": "district-1", + "name": "City of Lincoln" + } + ], + "precincts": [ + { + "id": "23", + "name": "North Lincoln" + }, + { + "id": "22", + "name": "South Lincoln" + }, + { + "id": "21", + "name": "East Lincoln" + }, + { + "id": "20", + "name": "West Lincoln" + } + ], + "ballotStyles": [ + { + "id": "1", + "precincts": ["20", "21", "22", "23"], + "districts": ["district-1"] + } + ], + "sealUrl": "/seals/state-of-hamilton-official-seal.svg", + "adjudicationReasons": [ + "UninterpretableBallot", + "Overvote", + "Undervote", + "BlankBallot" + ], + "markThresholds": { + "definite": 0.12, + "marginal": 0.12 + } +} diff --git a/apps/rave-jx-terminal/backend/tests/fixtures/robert_aikins_sample_cert.pem b/apps/rave-jx-terminal/backend/tests/fixtures/robert_aikins_sample_cert.pem new file mode 100644 index 0000000000..c08ff6d061 --- /dev/null +++ b/apps/rave-jx-terminal/backend/tests/fixtures/robert_aikins_sample_cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE7TCCA9WgAwIBAgICCbkwDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMx +GDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEMMAoGA1UECxMDRG9EMQwwCgYDVQQL +EwNQS0kxHTAbBgNVBAMTFERPRCBKSVRDIEVNQUlMIENBLTYzMB4XDTIzMDMwMjAw +MDAwMFoXDTI2MDMwMTIzNTk1OVowejELMAkGA1UEBhMCVVMxGDAWBgNVBAoTD1Uu +Uy4gR292ZXJubWVudDEMMAoGA1UECxMDRG9EMQwwCgYDVQQLEwNQS0kxDDAKBgNV +BAsTA1VTQTEnMCUGA1UEAxMeQUlLSU5TLlJPQkVSVC5FRERJRS4xNDA0OTIyMTAy +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Q5hlSvDkOe3qwN0SLAD +VCi5zUI6PO2c5yfCIOPqWwgN4VJ6GWamkE1XE2p2LP+BJ9+F5ig8cZ6o7hsFcrU0 +Z+sz32acZOxojiB4R4RYcstUery9OOzIcvtv5+mIWTy5nTaOYWAv+yeaIGrdU9nZ +ZoyISkf3fIA6UGCBN/Ai77Ljl7CZ2EWwRiFPuBUHClBH82evdq6c+EQdzZn0zipE +RDXN/kjXIZburWJx/vNffXXqUhizHIUZSEwcDQvbhT7X0X+YJWgdbHnYUQnZ6Tcv +JjEZnpcluRnxbzRIZrXXujd9eZ5xhjafQA0mgwEtB8eeow83LDE8b+xhNMX2zSm2 +zwIDAQABo4IBkzCCAY8wHwYDVR0jBBgwFoAUelWI79wGAVbXrwV+Dn69eBHD7lQw +QgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybC5uaXQuZGlzYS5taWwvY3JsL0RP +REpJVENFTUFJTENBXzYzLmNybDAOBgNVHQ8BAf8EBAMCBsAwFgYDVR0gBA8wDTAL +BglghkgBZQIBCyowHQYDVR0OBBYEFDn/WKbWy4sM26uI7lLXae3lfGLVMH4GCCsG +AQUFBwEBBHIwcDA+BggrBgEFBQcwAoYyaHR0cDovL2NybC5uaXQuZGlzYS5taWwv +c2lnbi9ET0RKSVRDRU1BSUxDQV82My5jZXIwLgYIKwYBBQUHMAGGImh0dHA6Ly9v +Y3NwLm5zbjAucmN2cy5uaXQuZGlzYS5taWwwIwYDVR0RBBwwGoEYcm9iZXJ0LmUu +YWlraW5zQGFybXkubWlsMBsGA1UdCQQUMBIwEAYIKwYBBQUHCQQxBBMCVVMwHwYD +VR0lBBgwFgYIKwYBBQUHAwQGCisGAQQBgjcKAwwwDQYJKoZIhvcNAQELBQADggEB +AH+YCZd2QkJTNWnv0Iq3t9ZD/vJoj7JmH9OMyBT81WENxOOOk8wHCBeIIaMHmK7q +bng0Rx6hK2SUUSCIEBKL3zXYjHKVCvYN3OvwQEQTOGDBti3oyK4yASWAod2gsN/P +ZM4u7IhD46mSpcXmfkcebZc48r3zCTpK/c69Py8hJjyG4hltnCHYSsTXyHkAe3qp +ssO5S99eJPOXsmO1q8hWF4+BD3s+eRxDhx3F9HWChGKeZ5OIQ7+dyRy8ae4nQlh+ +nex3FFNFr8KM4lotusdRQ4XcRFM7c7a3/epCH2LnYKixiBZWSU87pgfwvWW0AHGb +070GNtTwy5nUBhfyBrsCzV8= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/apps/rave-jx-terminal/frontend/.gitignore b/apps/rave-jx-terminal/frontend/.gitignore new file mode 100644 index 0000000000..ace9762350 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/dist/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + +# Added by cargo + +/target diff --git a/apps/rave-jx-terminal/frontend/Cargo.toml b/apps/rave-jx-terminal/frontend/Cargo.toml new file mode 100644 index 0000000000..147cc54b5c --- /dev/null +++ b/apps/rave-jx-terminal/frontend/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rave-jx-terminal-frontend" +version = "0.1.0" +authors = ["VotingWorks Eng "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = { workspace = true } +chrono = { version = "0.4.26", features = ["serde"] } +chrono-humanize = "0.2.3" +console_error_panic_hook = "0.1.7" +dioxus = { workspace = true } +dioxus-logger = { workspace = true } +dioxus-router = { workspace = true } +dioxus-web = { workspace = true } +getrandom = { version = "0.2", features = ["js"] } +js-sys = { workspace = true } +log = "0.4.19" +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +time.workspace = true +types-rs = { workspace = true } +ui-rs = { workspace = true } +wasm-bindgen = "0.2.87" +wasm-logger = "0.2.0" +web-sys = { version = "0.3.64", features = ["EventSource"] } diff --git a/apps/rave-jx-terminal/frontend/Dioxus.toml b/apps/rave-jx-terminal/frontend/Dioxus.toml new file mode 100644 index 0000000000..404bd61893 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/Dioxus.toml @@ -0,0 +1,51 @@ +[application] + +# App (Project) Name +name = "rave-jx-terminal-frontend" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "RAVE Jurisdiction" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# serve the root page when a route is not found +# this is necessary when using the `Router` component +index_on_404 = true + +# include `assets` in web platform +[web.resource] + +# CSS style file + +style = ["/styles.css"] + + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] + +[[web.proxy]] +backend = "http://127.0.0.1:5001/api" diff --git a/apps/rave-jx-terminal/frontend/README.md b/apps/rave-jx-terminal/frontend/README.md new file mode 100644 index 0000000000..6bfde857f5 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/README.md @@ -0,0 +1,48 @@ +# Development +{% if styling == "Tailwind" %} +1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm +2. Install the tailwind css cli: https://tailwindcss.com/docs/installation +3. Run the following command in the root of the project to start the tailwind CSS compiler: + +```bash +npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch +``` +{% endif %} +{% if platform == "desktop" %} +Run the following command in the root of the project to start the Dioxus dev server: + +```bash +dx serve --hot-reload --platform desktop +``` +{% else %} +{% if platform == "TUI" %} +Run the following command in the root of the project to start the Dioxus dev server: + +```bash +dx serve --hot-reload --platform desktop +``` +{% else %} +{% if platform == "web" %} +Run the following command in the root of the project to start the Dioxus dev server: + +```bash +dx serve --hot-reload +``` + +- Open the browser to http://localhost:8080 +{% else %} +{% if platform == "Fullstack" %} +Launch the Dioxus Fullstack app: + +```bash +dx build --features web --release +cargo run --features ssr --release +``` +{% else %} +Launch the Dioxus app: + +```bash +cargo run +``` +{% endif %} +{% endif %} \ No newline at end of file diff --git a/apps/rave-jx-terminal/frontend/input.css b/apps/rave-jx-terminal/frontend/input.css new file mode 100644 index 0000000000..bd6213e1df --- /dev/null +++ b/apps/rave-jx-terminal/frontend/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/apps/rave-jx-terminal/frontend/package.json b/apps/rave-jx-terminal/frontend/package.json new file mode 100644 index 0000000000..870474c070 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "@votingworks/rave-jx-terminal-frontend", + "version": "1.0.0", + "description": "RAVE Jurisdiction frontend", + "scripts": { + "build": "script/build", + "lint": "cargo fmt -- --check && cargo clippy -- -D warnings", + "start": "concurrently 'npm:start:*' 'npm:build:css:watch'", + "start:frontend": "dx serve --port 5000", + "start:backend": "pnpm --dir ../backend start", + "build:css:watch": "tailwindcss -i input.css -o ./public/styles.css --watch", + "test": "cargo test" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "dependencies": { + "tailwindcss": "^3.3.3" + }, + "devDependencies": { + "concurrently": "7.6.0" + }, + "packageManager": "pnpm@8.1.0" +} diff --git a/apps/rave-jx-terminal/frontend/public/styles.css b/apps/rave-jx-terminal/frontend/public/styles.css new file mode 100644 index 0000000000..b63ee1648b --- /dev/null +++ b/apps/rave-jx-terminal/frontend/public/styles.css @@ -0,0 +1,853 @@ +/* +! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.bottom-0 { + bottom: 0px; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.block { + display: block; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.hidden { + display: none; +} + +.h-screen { + height: 100vh; +} + +.w-1\/5 { + width: 20%; +} + +.w-4\/5 { + width: 80%; +} + +.w-full { + width: 100%; +} + +.w-screen { + width: 100vw; +} + +.table-auto { + table-layout: auto; +} + +.justify-center { + justify-content: center; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-l-md { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.border { + border-width: 1px; +} + +.border-2 { + border-width: 2px; +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + +.bg-green-300 { + --tw-bg-opacity: 1; + background-color: rgb(134 239 172 / var(--tw-bg-opacity)); +} + +.bg-orange-300 { + --tw-bg-opacity: 1; + background-color: rgb(253 186 116 / var(--tw-bg-opacity)); +} + +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity)); +} + +.bg-red-300 { + --tw-bg-opacity: 1; + background-color: rgb(252 165 165 / var(--tw-bg-opacity)); +} + +.bg-yellow-300 { + --tw-bg-opacity: 1; + background-color: rgb(253 224 71 / var(--tw-bg-opacity)); +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-8 { + padding: 2rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.pe-2 { + -webkit-padding-end: 0.5rem; + padding-inline-end: 0.5rem; +} + +.ps-0 { + -webkit-padding-start: 0px; + padding-inline-start: 0px; +} + +.ps-2 { + -webkit-padding-start: 0.5rem; + padding-inline-start: 0.5rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.font-semibold { + font-weight: 600; +} + +.italic { + font-style: italic; +} + +.text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-green-800 { + --tw-text-opacity: 1; + color: rgb(22 101 52 / var(--tw-text-opacity)); +} + +.text-orange-800 { + --tw-text-opacity: 1; + color: rgb(154 52 18 / var(--tw-text-opacity)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.text-yellow-800 { + --tw-text-opacity: 1; + color: rgb(133 77 14 / var(--tw-text-opacity)); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.hover\:cursor-pointer:hover { + cursor: pointer; +} + +.hover\:bg-gray-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.focus\:border-blue-500:focus { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.active\:bg-purple-700:active { + --tw-bg-opacity: 1; + background-color: rgb(126 34 206 / var(--tw-bg-opacity)); +} + +.disabled\:bg-purple-300:disabled { + --tw-bg-opacity: 1; + background-color: rgb(216 180 254 / var(--tw-bg-opacity)); +} + +@media (prefers-color-scheme: dark) { + .dark\:border-gray-600 { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); + } + + .dark\:bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); + } + + .dark\:bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); + } + + .dark\:text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); + } + + .dark\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + } + + .hover\:dark\:text-gray-700:hover { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); + } +} \ No newline at end of file diff --git a/apps/rave-jx-terminal/frontend/script/build b/apps/rave-jx-terminal/frontend/script/build new file mode 100755 index 0000000000..8e5208df7c --- /dev/null +++ b/apps/rave-jx-terminal/frontend/script/build @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! command dx --version &> /dev/null; then + cargo install dioxus-cli +fi + +rustup target add wasm32-unknown-unknown +dx build \ No newline at end of file diff --git a/apps/rave-jx-terminal/frontend/src/app.rs b/apps/rave-jx-terminal/frontend/src/app.rs new file mode 100644 index 0000000000..bc0989a178 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/app.rs @@ -0,0 +1,40 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_router::prelude::*; +use types_rs::rave::jx; +use wasm_bindgen::prelude::*; +use web_sys::MessageEvent; + +use crate::route::Route; + +pub fn App(cx: Scope) -> Element { + use_shared_state_provider(cx, jx::AppData::default); + let app_data = use_shared_state::(cx).unwrap(); + + use_coroutine(cx, { + to_owned![app_data]; + |_rx: UnboundedReceiver| async move { + let eventsource = web_sys::EventSource::new("/api/status-stream").unwrap(); + + let callback = Closure::wrap(Box::new(move |event: MessageEvent| { + if let Some(data) = event.data().as_string() { + match serde_json::from_str::(data.as_str()) { + Ok(new_app_data) => { + log::info!("new app data: {:?}", new_app_data); + *app_data.write() = new_app_data; + } + Err(err) => { + log::error!("error deserializing status event: {:?}", err); + } + } + } + }) as Box); + + eventsource.set_onmessage(Some(callback.as_ref().unchecked_ref())); + callback.forget(); + } + }); + + render!(Router:: {}) +} diff --git a/apps/rave-jx-terminal/frontend/src/components/election_configuration_cell.rs b/apps/rave-jx-terminal/frontend/src/components/election_configuration_cell.rs new file mode 100644 index 0000000000..3bf064b21f --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/components/election_configuration_cell.rs @@ -0,0 +1,40 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use types_rs::election::{BallotStyleId, ElectionHash, PrecinctId}; +use ui_rs::TableCell; + +#[derive(Debug, Props, PartialEq)] +pub struct Props { + #[props(into)] + election_title: String, + election_hash: ElectionHash, + precinct_id: PrecinctId, + ballot_style_id: BallotStyleId, +} + +pub fn ElectionConfigurationCell(cx: Scope) -> Element { + let election_title = &cx.props.election_title; + let election_hash = &cx.props.election_hash; + let precinct_id = &cx.props.precinct_id; + let ballot_style_id = &cx.props.ballot_style_id; + + render!( + TableCell { + p { + "{election_title}" + span { + class: "italic text-gray-400", + " ({election_hash.to_partial()})" + } + } + p { + "{ballot_style_id} / {precinct_id}" + span { + class: "italic text-gray-400", + " (Ballot Style / Precinct)" + } + } + } + ) +} diff --git a/apps/rave-jx-terminal/frontend/src/components/mod.rs b/apps/rave-jx-terminal/frontend/src/components/mod.rs new file mode 100644 index 0000000000..9fa985affc --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/components/mod.rs @@ -0,0 +1,3 @@ +mod election_configuration_cell; + +pub use election_configuration_cell::ElectionConfigurationCell; diff --git a/apps/rave-jx-terminal/frontend/src/layout.rs b/apps/rave-jx-terminal/frontend/src/layout.rs new file mode 100644 index 0000000000..99a7b02fa9 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/layout.rs @@ -0,0 +1,37 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use dioxus_router::prelude::*; + +use crate::route::Route; + +pub fn Layout(cx: Scope) -> Element { + render!( + div { + class: "h-screen w-screen flex dark:bg-gray-800 dark:text-gray-300", + div { + class: "w-1/5 bg-gray-200 dark:bg-gray-700", + ul { + class: "mt-8", + for route in [Route::ElectionsPage, Route::VotersPage, Route::BallotsPage] { + li { + Link { + to: route.clone(), + active_class: "bg-gray-300 dark:bg-gray-800", + class: "px-4 py-2 block hover:bg-gray-300 dark:bg-gray-700 hover:dark:text-gray-700 hover:cursor-pointer", + "{route.label()}" + } + } + } + li { + class: "fixed bottom-0 w-1/5 font-bold text-center py-2", + "RAVE Jurisdiction" + } + } + } + div { class: "w-4/5 p-8", + Outlet:: {} + } + } + ) +} diff --git a/apps/rave-jx-terminal/frontend/src/main.rs b/apps/rave-jx-terminal/frontend/src/main.rs new file mode 100644 index 0000000000..7dafa3b05b --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/main.rs @@ -0,0 +1,18 @@ +#![allow(non_snake_case)] + +use log::LevelFilter; + +mod app; +mod components; +mod layout; +mod pages; +mod route; +mod util; + +fn main() { + dioxus_logger::init(LevelFilter::Info).expect("failed to init logger"); + console_error_panic_hook::set_once(); + + log::info!("starting app"); + dioxus_web::launch(crate::app::App); +} diff --git a/apps/rave-jx-terminal/frontend/src/pages/ballots_page.rs b/apps/rave-jx-terminal/frontend/src/pages/ballots_page.rs new file mode 100644 index 0000000000..5e70b4358e --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/pages/ballots_page.rs @@ -0,0 +1,242 @@ +use dioxus::prelude::*; +use types_rs::{cdf::cvr::Cvr, rave::jx}; +use ui_rs::DateOrDateTimeCell; + +use crate::components::ElectionConfigurationCell; + +#[derive(PartialEq, Props)] +struct VotersProps<'a> { + app_data: &'a jx::AppData, +} + +pub fn BallotsPage(cx: Scope) -> Element { + let app_data = use_shared_state::(cx).unwrap(); + let app_data = app_data.read(); + let elections = app_data.elections.clone(); + let printed_ballots = app_data.printed_ballots.clone(); + let scanned_ballots = app_data.scanned_ballots.clone(); + + render!( + h1 { class: "text-2xl font-bold mb-4", "Printed Ballots" } + if printed_ballots.is_empty() { + rsx!("No printed ballots") + } else { + to_owned![elections, printed_ballots]; + rsx!(PrintedBallotsTable { + elections: elections, + printed_ballots: printed_ballots, + }) + } + + h1 { class: "text-2xl font-bold mt-4 mb-4", "Scanned Ballots" } + if scanned_ballots.is_empty() { + rsx!("No scanned ballots") + } else { + to_owned![elections, scanned_ballots]; + rsx!(ScannedBallotsTable { + elections: elections, + scanned_ballots: scanned_ballots, + }) + } + ) +} + +#[derive(PartialEq, Props)] +struct PendingRegistrationsTableProps { + elections: Vec, + printed_ballots: Vec, +} + +fn summarize_cast_vote_record(cvr: Cvr) -> String { + let mut summary = String::new(); + + for snapshot in cvr.cvr_snapshot { + if let Some(contests) = snapshot.cvr_contest { + for contest in contests { + if let Some(contest_selections) = contest.cvr_contest_selection { + for contest_selection in contest_selections { + if let Some(contest_selection_id) = contest_selection.contest_selection_id { + summary.push_str(&format!( + "{}: {}\n", + contest.contest_id, contest_selection_id + )); + } + } + } + } + } + } + + summary +} + +fn PrintedBallotsTable(cx: Scope) -> Element { + let elections = &cx.props.elections; + + let get_election_by_id = |election_id| { + elections + .iter() + .find(|election| election.id() == election_id) + }; + + render!( + div { + rsx!( + table { class: "table-auto w-full", + thead { + tr { + th { class: "px-4 py-2 text-left", "Election Configuration" } + th { class: "px-4 py-2 text-left", "Cast Vote Record" } + th { class: "px-4 py-2 text-left", "Created At" } + } + } + tbody { + for printed_ballot in cx.props.printed_ballots.iter() { + { + let election = get_election_by_id(printed_ballot.election_id()).unwrap(); + + rsx!(tr { + ElectionConfigurationCell { + election_title: election.title.clone(), + election_hash: election.election_hash.clone(), + precinct_id: printed_ballot.precinct_id().clone(), + ballot_style_id: printed_ballot.ballot_style_id().clone(), + } + td { + class: "border px-4 py-2 whitespace-nowrap", + match printed_ballot.cast_vote_record() { + Ok(cvr) => { + rsx!( + match &printed_ballot.verification_status { + jx::VerificationStatus::Success { common_access_card_id, display_name } => { + rsx!(span { + class: "text-sm p-1 ps-0 pe-2 text-green-800 bg-green-300 font-semibold rounded-md", + title: "{display_name}", + span { + class: "text-sm p-1 ps-2 pe-2 text-white bg-gray-400 font-semibold rounded-l-md", + "CAC #{common_access_card_id}" + } + span { + class: "ps-2", + "Verified" + } + }) + } + jx::VerificationStatus::Failure => { + rsx!(span { + class: "text-sm p-1 ps-0 pe-2 text-red-800 bg-red-300 font-semibold rounded-md", + span { + class: "text-sm p-1 ps-2 pe-2 text-white bg-gray-400 font-semibold rounded-l-md", + "CAC" + } + span { + class: "ps-2", + "Unverified" + } + }) + } + jx::VerificationStatus::Error(err) => { + rsx!(span { + class: "text-sm p-1 ps-0 pe-2 text-orange-800 bg-orange-300 font-semibold rounded-md", + span { + class: "text-sm p-1 ps-2 pe-2 text-white bg-gray-400 font-semibold rounded-l-md", + "CAC" + } + span { + class: "ps-2", + title: "{err}", + "Error" + } + }) + } + jx::VerificationStatus::Unknown => { + rsx!(span { + class: "text-sm p-1 ps-0 pe-2 text-yellow-800 bg-yellow-300 font-semibold rounded-md", + span { + class: "text-sm p-1 ps-2 pe-2 text-white bg-gray-400 font-semibold rounded-l-md", + "CAC" + } + span { + class: "ps-2", + "Unknown" + } + }) + } + } + details { + rsx!(summary { + class: "text-gray-200", + "DEBUG" + }) + { + let summary = summarize_cast_vote_record(cvr); + rsx!(pre { + summary + }) + } + } + ) + } + Err(e) => { + rsx!(p { + class: "text-sm text-red-500", + "Cast vote record is invalid: {e}" + }) + } + } + + } + DateOrDateTimeCell { + date_or_datetime: printed_ballot.created_at(), + } + }) + } + } + } + } + ) + } + ) +} + +#[derive(Debug, PartialEq, Props)] +struct RegistrationsTableProps { + elections: Vec, + scanned_ballots: Vec, +} + +fn ScannedBallotsTable(cx: Scope) -> Element { + render!( + table { class: "table-auto w-full", + thead { + tr { + th { class: "px-4 py-2 text-left", "Election" } + th { class: "px-4 py-2 text-left", "Created At" } + } + } + tbody { + for scanned_ballot in cx.props.scanned_ballots.iter() { + tr { + { + let election = cx + .props + .elections + .iter() + .find(|election| *election.id() == scanned_ballot.election_id) + .unwrap(); + rsx!(ElectionConfigurationCell { + election_title: election.title.clone(), + election_hash: election.election_hash.clone(), + precinct_id: scanned_ballot.precinct_id.clone(), + ballot_style_id: scanned_ballot.ballot_style_id.clone(), + }) + } + DateOrDateTimeCell { + date_or_datetime: scanned_ballot.created_at() + } + } + } + } + } + ) +} diff --git a/apps/rave-jx-terminal/frontend/src/pages/elections_page.rs b/apps/rave-jx-terminal/frontend/src/pages/elections_page.rs new file mode 100644 index 0000000000..d05c168156 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/pages/elections_page.rs @@ -0,0 +1,106 @@ +#![allow(non_snake_case)] + +use std::sync::Arc; + +use dioxus::prelude::*; +use types_rs::rave::jx; +use ui_rs::{DateOrDateTimeCell, FileButton}; + +use crate::util::{file::read_file_as_bytes, url::get_url}; + +pub fn ElectionsPage(cx: Scope) -> Element { + let app_data = use_shared_state::(cx).unwrap(); + let elections = &app_data.read().elections; + let is_uploading = use_state(cx, || false); + let upload_election = { + to_owned![is_uploading]; + |election_data: Vec| async move { + is_uploading.set(true); + + let url = get_url("/api/elections"); + let client = reqwest::Client::new(); + let res = client + .post(url) + .body(election_data) + .header("Content-Type", "application/json") + .send() + .await; + + is_uploading.set(false); + + res + } + }; + + render! ( + div { + h1 { class: "text-2xl font-bold mb-4", "Elections" } + if elections.is_empty() { + rsx!(div { "No elections found." }) + } else { + rsx!(table { class: "table-auto w-full", + thead { + tr { + th { class: "px-4 py-2 text-left", "Election ID" } + th { class: "px-4 py-2 text-left", "Title" } + th { class: "px-4 py-2 text-left", "Date" } + th { class: "px-4 py-2 text-left", "Synced" } + th { class: "px-4 py-2 text-left", "Created At" } + } + } + tbody { + for election in elections.iter() { + tr { + td { + class: "border px-4 py-2", + title: "Database ID: {election.id}\n\nFull Election Hash: {election.election_hash}", + "{election.election_hash.to_partial()}" + } + td { class: "border px-4 py-2", "{election.title}" } + DateOrDateTimeCell { + date_or_datetime: election.date, + } + td { class: "border px-4 py-2", if election.is_synced() { "Yes" } else { "No" } } + DateOrDateTimeCell { + date_or_datetime: election.created_at, + } + } + } + } + }) + } + FileButton { + "Import Election", + class: "mt-4", + onfile: move |file_engine: Arc| { + cx.spawn({ + to_owned![upload_election, file_engine]; + async move { + if let Some(election_data) = read_file_as_bytes(file_engine).await { + match upload_election(election_data).await { + Ok(response) => { + if !response.status().is_success() { + web_sys::window() + .unwrap() + .alert_with_message(format!("Error uploading election: {}", response.status().as_str()).as_str()) + .unwrap(); + return; + } + + log::info!("uploaded election: {:?}", response); + } + Err(err) => { + web_sys::window() + .unwrap() + .alert_with_message(format!("Error uploading election: {:?}", err).as_str()) + .unwrap(); + } + } + }; + } + }); + }, + } + } + ) +} diff --git a/apps/rave-jx-terminal/frontend/src/pages/mod.rs b/apps/rave-jx-terminal/frontend/src/pages/mod.rs new file mode 100644 index 0000000000..6ba4453fce --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/pages/mod.rs @@ -0,0 +1,7 @@ +mod ballots_page; +mod elections_page; +mod voters_page; + +pub use ballots_page::BallotsPage; +pub use elections_page::ElectionsPage; +pub use voters_page::VotersPage; diff --git a/apps/rave-jx-terminal/frontend/src/pages/voters_page.rs b/apps/rave-jx-terminal/frontend/src/pages/voters_page.rs new file mode 100644 index 0000000000..7f741839d9 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/pages/voters_page.rs @@ -0,0 +1,209 @@ +use dioxus::prelude::*; +use types_rs::rave::jx; +use ui_rs::DateOrDateTimeCell; + +use crate::{components::ElectionConfigurationCell, util::url::get_url}; + +#[derive(PartialEq, Props)] +struct VotersProps<'a> { + app_data: &'a jx::AppData, +} + +pub fn VotersPage(cx: Scope) -> Element { + let app_data = use_shared_state::(cx).unwrap(); + let app_data = app_data.read(); + let elections = app_data.elections.clone(); + let registration_requests = app_data.registration_requests.clone(); + let registrations = app_data.registrations.clone(); + let pending_registration_requests = registration_requests + .iter() + .filter(|registration_request| { + !registrations + .iter() + .any(|registration| registration.is_registration_request(registration_request)) + }) + .map(Clone::clone) + .collect::>(); + + render!( + h1 { class: "text-2xl font-bold mb-4", "Pending Registrations" } + if pending_registration_requests.is_empty() { + rsx!("No pending registrations") + } else { + rsx!(PendingRegistrationsTable { + elections: elections, + pending_registration_requests: pending_registration_requests, + }) + } + + h1 { class: "text-2xl font-bold mt-4 mb-4", "Registrations" } + if registrations.is_empty() { + rsx!("No registrations") + } else { + rsx!(RegistrationsTable { + registrations: registrations, + }) + } + ) +} + +#[derive(PartialEq, Props)] +struct PendingRegistrationsTableProps { + elections: Vec, + pending_registration_requests: Vec, +} + +fn PendingRegistrationsTable(cx: Scope) -> Element { + let elections = &cx.props.elections; + let pending_registration_requests = &cx.props.pending_registration_requests; + + // let is_linking_registration_request_with_election = use_state(cx, || false); + + let link_voter_registration_request_and_election = { + // TODO: make this work + // to_owned![is_linking_registration_request_with_election]; + |create_registration_data: jx::CreateRegistrationData| async move { + // is_linking_registration_request_with_election.set(true); + + let url = get_url("/api/registrations"); + let client = reqwest::Client::new(); + client + .post(url) + .json(&create_registration_data) + .send() + .await + // is_linking_registration_request_with_election.set(false); + } + }; + + render!( + div { + rsx!( + table { class: "table-auto w-full", + thead { + tr { + th { class: "px-4 py-2 text-left", "Voter Name" } + th { class: "px-4 py-2 text-left", "Voter CAC ID" } + th { class: "px-4 py-2 text-left", "Election Configuration" } + th { class: "px-4 py-2 text-left", "Created At" } + } + } + tbody { + for registration_request in pending_registration_requests { + tr { + td { class: "border px-4 py-2", "{registration_request.display_name()}" } + td { class: "border px-4 py-2", "{registration_request.common_access_card_id()}" } + td { + class: "border px-4 py-2 justify-center", + select { + class: "dark:bg-gray-800 dark:text-white dark:border-gray-600 border-2 rounded-md p-2 focus:outline-none focus:border-blue-500", + oninput: move |event| { + let create_registration_data = serde_json::from_str::(event.inner().value.as_str()).expect("parse succeeded"); + cx.spawn({ + to_owned![link_voter_registration_request_and_election, create_registration_data]; + async move { + log::info!("linking registration request: {create_registration_data:?}"); + match link_voter_registration_request_and_election(create_registration_data).await { + Ok(response) => { + if !response.status().is_success() { + web_sys::window() + .unwrap() + .alert_with_message(format!("Error linking registration request to election: {}", response.status().as_str()).as_str()) + .unwrap(); + return; + } + + log::info!("linked registration request to election: {:?}", response); + } + Err(err) => { + web_sys::window() + .unwrap() + .alert_with_message(format!("Error linking registration request to election: {:?}", err).as_str()) + .unwrap(); + } + } + } + }) + }, + option { + value: "", + disabled: true, + "Select election configuration" + } + for election in elections.iter() { + optgroup { + label: "{election.title} ({election.election_hash.to_partial()})", + for ballot_style in election.ballot_styles.iter() { + for precinct_id in ballot_style.precincts.iter() { + { + let create_registration_data = jx::CreateRegistrationData { + election_id: election.id, + registration_request_id: *registration_request.id(), + ballot_style_id: ballot_style.id.clone(), + precinct_id: precinct_id.clone(), + }; + let value = serde_json::to_string(&create_registration_data) + .expect("serialization succeeds"); + rsx!( + option { + value: "{value}", + "{ballot_style.id} / {precinct_id}" + } + ) + } + } + } + } + } + } + } + DateOrDateTimeCell { + date_or_datetime: registration_request.created_at(), + } + } + } + } + } + ) + } + ) +} + +#[derive(Debug, PartialEq, Props)] +struct RegistrationsTableProps { + registrations: Vec, +} + +fn RegistrationsTable(cx: Scope) -> Element { + render!( + table { class: "table-auto w-full", + thead { + tr { + th { class: "px-4 py-2 text-left", "Voter Name" } + th { class: "px-4 py-2 text-left", "Voter CAC ID" } + th { class: "px-4 py-2 text-left", "Election Configuration" } + th { class: "px-4 py-2 text-left", "Synced" } + th { class: "px-4 py-2 text-left", "Created At" } + } + } + tbody { + for registration in cx.props.registrations.iter() { + tr { + td { class: "border px-4 py-2", "{registration.display_name()}" } + td { class: "border px-4 py-2", "{registration.common_access_card_id()}" } + ElectionConfigurationCell { + election_title: registration.election_title(), + election_hash: registration.election_hash().clone(), + precinct_id: registration.precinct_id().clone(), + ballot_style_id: registration.ballot_style_id().clone(), + } + td { class: "border px-4 py-2", if registration.is_synced() { "Yes" } else { "No" } } + DateOrDateTimeCell { + date_or_datetime: registration.created_at() + } + } + } + } + } + ) +} diff --git a/apps/rave-jx-terminal/frontend/src/route.rs b/apps/rave-jx-terminal/frontend/src/route.rs new file mode 100644 index 0000000000..a26e9a3e93 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/route.rs @@ -0,0 +1,30 @@ +use dioxus::prelude::*; +use dioxus_router::prelude::*; + +use crate::layout::Layout; +use crate::pages::BallotsPage; +use crate::pages::ElectionsPage; +use crate::pages::VotersPage; + +#[derive(Clone, Debug, PartialEq, Routable)] +#[allow(clippy::enum_variant_names)] +pub enum Route { + #[layout(Layout)] + #[redirect("/", || Route::ElectionsPage)] + #[route("/elections")] + ElectionsPage, + #[route("/voters")] + VotersPage, + #[route("/ballots")] + BallotsPage, +} + +impl Route { + pub fn label(&self) -> &'static str { + match self { + Self::ElectionsPage => "Elections", + Self::VotersPage => "Voters", + Self::BallotsPage => "Ballots", + } + } +} diff --git a/apps/rave-jx-terminal/frontend/src/util/file.rs b/apps/rave-jx-terminal/frontend/src/util/file.rs new file mode 100644 index 0000000000..60d1b8175f --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/util/file.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +use dioxus::prelude::FileEngine; + +pub async fn read_file_as_bytes(file_engine: Arc) -> Option> { + let files = file_engine.files(); + let file = files.first()?; + file_engine.read_file(file).await +} diff --git a/apps/rave-jx-terminal/frontend/src/util/mod.rs b/apps/rave-jx-terminal/frontend/src/util/mod.rs new file mode 100644 index 0000000000..3bb10f97a9 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/util/mod.rs @@ -0,0 +1,2 @@ +pub mod file; +pub mod url; diff --git a/apps/rave-jx-terminal/frontend/src/util/url.rs b/apps/rave-jx-terminal/frontend/src/util/url.rs new file mode 100644 index 0000000000..175e5f63bb --- /dev/null +++ b/apps/rave-jx-terminal/frontend/src/util/url.rs @@ -0,0 +1,8 @@ +pub fn get_root_url() -> reqwest::Url { + let loc = web_sys::window().unwrap().location(); + reqwest::Url::parse(loc.origin().unwrap().as_str()).unwrap() +} + +pub fn get_url(path: &str) -> reqwest::Url { + get_root_url().join(path).unwrap() +} diff --git a/apps/rave-jx-terminal/frontend/tailwind.config.js b/apps/rave-jx-terminal/frontend/tailwind.config.js new file mode 100644 index 0000000000..e584227552 --- /dev/null +++ b/apps/rave-jx-terminal/frontend/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: 'all', + content: [ + './src/**/*.{rs,html,css}', + './dist/**/*.html', + '../../../libs/ui-rs/src/**/*.{rs,html,css}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/rave-mark/Makefile b/apps/rave-mark/Makefile new file mode 100644 index 0000000000..e5ad8dd90b --- /dev/null +++ b/apps/rave-mark/Makefile @@ -0,0 +1,35 @@ +APP := rave-mark + +ALL: build + +clean: clean-frontend clean-backend + +clean-frontend: + @echo "🧹 Cleaning frontend…" + @cd frontend && pnpm clean + +clean-backend: + @echo "🧹 Cleaning backend…" + @cd backend && pnpm clean + +build: build-frontend build-backend + +build-frontend: + @echo "🛠️ Building frontend…" + @cd frontend && pnpm build + +build-backend: + @echo "🛠️ Building backend…" + @cd backend && pnpm build + +dist: + @echo "❌ Not implemented yet" + @exit 1 + +run: build + @echo "🚀 Running application in production mode…" + @cd frontend && NODE_ENV=production pnpm concurrently --kill-others --names frontend,backend "pnpm --dir prodserver start" "pnpm --dir ../backend start" + +reset-db: + @echo "🗑️ Resetting database…" + @test -d "${RAVE_MARK_WORKSPACE}" && rm -rf "${RAVE_MARK_WORKSPACE}" && echo "✅ Database reset" || echo "❌ Database not reset, no RAVE_MARK_WORKSPACE" \ No newline at end of file diff --git a/apps/rave-mark/backend/Makefile b/apps/rave-mark/backend/Makefile new file mode 100644 index 0000000000..a1b8c83334 --- /dev/null +++ b/apps/rave-mark/backend/Makefile @@ -0,0 +1,14 @@ +# a phony dependency that can be used as a dependency to force builds +FORCE: + +build: FORCE + pnpm install && pnpm build + +bootstrap: build + +install: + sudo apt -y update + sudo apt -y install libpcsclite1 libpcsclite-dev + +run: + pnpm start diff --git a/apps/rave-mark/backend/package.json b/apps/rave-mark/backend/package.json index 3f3a9dc917..e932bc0bf0 100644 --- a/apps/rave-mark/backend/package.json +++ b/apps/rave-mark/backend/package.json @@ -18,17 +18,16 @@ "lint": "pnpm type-check && eslint .", "lint:fix": "pnpm type-check && eslint . --fix", "pre-commit": "lint-staged", - "start": "node ./build/index.js", + "start": "TZ=UTC VX_MACHINE_TYPE=rave-mark node ./build/index.js", "test": "is-ci test:ci test:watch", - "test:ci": "jest --coverage --reporters=default --reporters=jest-junit --maxWorkers=7", - "test:coverage": "jest --coverage", - "test:debug": "node --inspect-brk $(which jest) --runInBand --no-cache", - "test:watch": "jest --watch", + "test:ci": "VX_MACHINE_TYPE=rave-mark jest --coverage --reporters=default --reporters=jest-junit --maxWorkers=7", + "test:coverage": "VX_MACHINE_TYPE=rave-mark jest --coverage", + "test:debug": "VX_MACHINE_TYPE=rave-mark node --inspect-brk $(which jest) --runInBand --no-cache", + "test:watch": "VX_MACHINE_TYPE=rave-mark jest --watch", "type-check": "tsc --build" }, "lint-staged": { "*.+(js|jsx|ts|tsx)": [ - "stylelint", "eslint --quiet --fix" ], "*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)": [ @@ -43,50 +42,50 @@ "@votingworks/backend": "workspace:*", "@votingworks/basics": "workspace:*", "@votingworks/db": "workspace:*", - "@votingworks/dev-dock-backend": "workspace:*", "@votingworks/fixtures": "workspace:*", "@votingworks/grout": "workspace:*", "@votingworks/logging": "workspace:*", "@votingworks/types": "workspace:*", "@votingworks/utils": "workspace:*", - "debug": "^4.3.4", + "cross-fetch": "^3.1.5", + "debug": "4.3.4", "dotenv": "^16.1.4", "dotenv-expand": "^10.0.0", - "express": "^4.18.2", - "fs-extra": "^11.1.1", + "express": "4.18.2", + "fs-extra": "11.1.1", + "luxon": "^3.0.0", + "puppeteer": "^21.1.1", + "tmp": "^0.2.1", + "uuid": "9.0.1", + "xml": "^1.0.1", "zod": "3.14.4" }, "devDependencies": { - "@types/debug": "^4.1.8", - "@types/express": "^4.17.14", - "@types/fs-extra": "^11.0.1", - "@types/jest": "^29.5.2", - "@types/tmp": "^0.2.3", - "@typescript-eslint/eslint-plugin": "5.37.0", - "@typescript-eslint/parser": "5.37.0", + "@types/debug": "4.1.8", + "@types/express": "4.17.14", + "@types/fs-extra": "11.0.1", + "@types/jest": "^29.5.3", + "@types/luxon": "^3.0.0", + "@types/node": "16.18.23", + "@types/tmp": "0.2.4", + "@types/uuid": "9.0.5", + "@types/xml": "^1.0.9", "@votingworks/test-utils": "workspace:*", - "eslint": "8.23.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-node": "^0.3.4", + "eslint": "8.51.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-node": "^0.3.9", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^26.1.5", - "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-jest": "^27.2.3", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-vx": "workspace:*", - "is-ci-cli": "^2.1.2", - "jest": "^29.5.0", - "jest-junit": "^14.0.1", - "jest-watch-typeahead": "^0.6.4", - "lint-staged": "^10.5.3", + "is-ci-cli": "2.2.0", + "jest": "^29.6.2", + "jest-junit": "^16.0.0", + "jest-watch-typeahead": "^2.2.2", + "lint-staged": "11.0.0", "nodemon": "^2.0.20", - "prettier": "2.6.2", - "stylelint": "^13.3.3", - "stylelint-config-palantir": "^4.0.1", - "stylelint-config-prettier": "^8.0.1", - "stylelint-config-styled-components": "^0.1.1", - "stylelint-processor-styled-components": "^1.10.0", - "tmp": "^0.2.1", - "ts-jest": "^29.1.0", - "typescript": "4.6.3" + "prettier": "3.0.3", + "ts-jest": "29.1.1" }, "packageManager": "pnpm@8.1.0" } diff --git a/apps/rave-mark/backend/schema.sql b/apps/rave-mark/backend/schema.sql index e69de29bb2..b0c369af6b 100644 --- a/apps/rave-mark/backend/schema.sql +++ b/apps/rave-mark/backend/schema.sql @@ -0,0 +1,122 @@ +create table system_settings ( + -- enforce singleton table + id integer primary key check (id = 1), + data text not null -- JSON blob +); + +create table server_sync_attempts ( + id uuid primary key, + creator text not null, + trigger text not null, + status_message text not null, + success boolean, + created_at timestamptz not null default current_timestamp, + completed_at timestamp +); + +create table elections ( + -- generated on this machine + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid, + -- generated on a client machine; should match `id` if this record was + -- generated on this machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id text not null, + definition bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table admins ( + machine_id varchar(255) not null, + -- CAC ID of the admin user + common_access_card_id uuid not null unique, + created_at timestamptz not null default current_timestamp +); + +create table registration_requests ( + -- generated on this machine + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid, + -- generated on a client machine; should match `id` if this record was + -- generated on this machine + client_id uuid not null unique, + -- ID of the machine this record was originally created on + machine_id text not null, + -- CAC ID of the person for this record + common_access_card_id uuid not null unique, + given_name text not null, + family_name text not null, + address_line_1 text not null, + address_line_2 text, + city text not null, + state text not null, + postal_code text not null, + -- the state-issued id number of the person making the request, + -- e.g. a driver's license number + state_id text not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table registrations ( + -- generated on this machine + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid, + -- generated on a client machine; should match `id` if this record was + -- generated on this machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id text not null, + -- CAC ID of the person for this record + common_access_card_id uuid not null unique, + registration_request_id uuid not null references registration_requests(id), + election_id uuid not null references elections(id), + precinct_id text not null, + ballot_style_id text not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table printed_ballots ( + -- generated on this machine + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid, + -- generated on a client machine; should match `id` if this record was + -- generated on this machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id text not null, + -- CAC ID of the person for this record + common_access_card_id uuid not null unique, + common_access_card_certificate bytea not null, + registration_id uuid not null references registrations(id), + cast_vote_record bytea not null, + cast_vote_record_signature bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table scanned_ballots ( + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid unique, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_id uuid not null references elections(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); diff --git a/apps/rave-mark/backend/src/app.ts b/apps/rave-mark/backend/src/app.ts index eb098c3bf0..b33796b253 100644 --- a/apps/rave-mark/backend/src/app.ts +++ b/apps/rave-mark/backend/src/app.ts @@ -1,17 +1,521 @@ -import { useDevDockRouter } from '@votingworks/dev-dock-backend'; +import { buildCastVoteRecord, VX_MACHINE_ID } from '@votingworks/backend'; +import { + Optional, + Result, + assert, + err, + find, + iter, + ok, +} from '@votingworks/basics'; import * as grout from '@votingworks/grout'; +import { + BallotIdSchema, + BallotStyleId, + BallotType, + Id, + PrecinctId, + VotesDict, + unsafeParse, +} from '@votingworks/types'; +import { Buffer } from 'buffer'; import express, { Application } from 'express'; +import { isDeepStrictEqual } from 'util'; +import { execFileSync } from 'child_process'; +import { IS_INTEGRATION_TEST } from './globals'; +import * as mailingLabel from './mailing_label'; +import { RaveServerClient } from './rave_server_client'; +import { Auth, AuthStatus } from './types/auth'; +import { ClientId, RegistrationRequest } from './types/db'; +import { Workspace } from './workspace'; -function buildApi() { - return grout.createApi({}); +export type VoterStatus = + | 'unregistered' + | 'registration_pending' + | 'registered' + | 'voted'; + +export interface CreateTestVoterInput { + /** + * Whether or not the voter should be an admin. + */ + isAdmin?: boolean; + + registrationRequest?: { + /** + * Voter's given name, i.e. first name. + */ + givenName?: string; + + /** + * Voter's family name, i.e. last name. + */ + familyName?: string; + + /** + * Voter's address line 1. + */ + addressLine1?: string; + + /** + * Voter's address line 2. + */ + addressLine2?: string; + + /** + * Voter's city. + */ + city?: string; + + /** + * Voter's state. + */ + state?: string; + + /** + * Voter's postal code. + */ + postalCode?: string; + + /** + * Voter's state ID. + */ + stateId?: string; + }; + + registration?: { + /** + * Election definition as a JSON string. + */ + electionData?: string; + + /** + * Precinct ID to register the voter to. + */ + precinctId?: PrecinctId; + + /** + * Ballot style ID to register the voter to. + */ + ballotStyleId?: BallotStyleId; + }; +} + +function buildApi({ + auth, + workspace, + raveServerClient, +}: { + auth: Auth; + workspace: Workspace; + raveServerClient: RaveServerClient; +}) { + async function getAuthStatus(): Promise { + return await auth.getAuthStatus(); + } + + function assertIsIntegrationTest() { + if (!IS_INTEGRATION_TEST) { + throw new Error('This is not an integration test'); + } + } + + return grout.createApi({ + getAuthStatus, + + checkPin(input: { pin: string }) { + return auth.checkPin(input.pin); + }, + + async getVoterStatus(): Promise< + Optional<{ status: VoterStatus; isAdmin: boolean }> + > { + const authStatus: AuthStatus = await getAuthStatus(); + + if (authStatus.status !== 'has_card') { + return undefined; + } + + const { commonAccessCardId } = authStatus.card; + const isAdmin = workspace.store.isAdmin(commonAccessCardId); + const registrations = + workspace.store.getRegistrations(commonAccessCardId); + + if (registrations.length === 0) { + const registrationRequests = + workspace.store.getRegistrationRequests(commonAccessCardId); + + return { + status: + registrationRequests.length > 0 + ? 'registration_pending' + : 'unregistered', + isAdmin, + }; + } + + // TODO: support multiple registrations + const registration = registrations[0]; + assert(registration); + const selection = + workspace.store.getPrintedBallotCastVoteRecordForRegistration( + registration.id + ); + + return { status: selection ? 'voted' : 'registered', isAdmin }; + }, + + async getRegistrationRequests(): Promise { + const authStatus = await getAuthStatus(); + + if (authStatus.status !== 'has_card') { + return []; + } + + return workspace.store.getRegistrationRequests( + authStatus.card.commonAccessCardId + ); + }, + + async createVoterRegistration(input: { + givenName: string; + familyName: string; + addressLine1: string; + addressLine2?: string; + city: string; + state: string; + postalCode: string; + stateId: string; + pin: string; + }): Promise< + Result< + { id: Id }, + { type: 'not_logged_in' | 'incorrect_pin'; message: string } + > + > { + const authStatus = await getAuthStatus(); + + if (authStatus.status !== 'has_card') { + return err({ type: 'not_logged_in', message: 'Not logged in' }); + } + + if (!(await auth.checkPin(input.pin))) { + return err({ type: 'incorrect_pin', message: 'Incorrect PIN' }); + } + + const id = ClientId(); + workspace.store.createRegistrationRequest({ + id, + commonAccessCardId: authStatus.card.commonAccessCardId, + givenName: input.givenName, + familyName: input.familyName, + addressLine1: input.addressLine1, + addressLine2: input.addressLine2, + city: input.city, + state: input.state, + postalCode: input.postalCode, + stateId: input.stateId, + }); + return ok({ id }); + }, + + async getElectionConfiguration() { + const authStatus = await getAuthStatus(); + + if (authStatus.status !== 'has_card') { + return undefined; + } + + const { commonAccessCardId } = authStatus.card; + const registrations = + workspace.store.getRegistrations(commonAccessCardId); + // TODO: Handle multiple registrations + const registration = registrations[0]; + + if (!registration) { + return undefined; + } + + const electionDefinition = workspace.store.getRegistrationElection( + registration.id + ); + + if (!electionDefinition) { + return undefined; + } + + return { + electionDefinition, + ballotStyleId: registration.ballotStyleId, + precinctId: registration.precinctId, + }; + }, + + async castBallot(input: { votes: VotesDict; pin: string }) { + const authStatus = await getAuthStatus(); + + if (authStatus.status !== 'has_card') { + throw new Error('Not logged in'); + } + + const { commonAccessCardId } = authStatus.card; + const registrations = + workspace.store.getRegistrations(commonAccessCardId); + // TODO: Handle multiple registrations + const registration = registrations[0]; + + if (!registration) { + throw new Error('Not registered'); + } + + const electionDefinition = workspace.store.getRegistrationElection( + registration.id + ); + + if (!electionDefinition) { + throw new Error('no election definition found for registration'); + } + + const ballotId = ClientId(); + const castVoteRecordId = unsafeParse(BallotIdSchema, ballotId); + const castVoteRecord = buildCastVoteRecord({ + election: electionDefinition.election, + electionId: electionDefinition.electionHash, + scannerId: VX_MACHINE_ID, + // TODO: what should the batch ID be? + batchId: '', + castVoteRecordId, + interpretation: { + type: 'InterpretedBmdPage', + metadata: { + ballotStyleId: registration.ballotStyleId, + precinctId: registration.precinctId, + ballotType: BallotType.Absentee, + electionHash: electionDefinition.electionHash, + // TODO: support test mode + isTestMode: false, + }, + votes: input.votes, + }, + ballotMarkingMode: 'machine', + }); + + const commonAccessCardCertificate = await auth.getCertificate(); + assert(commonAccessCardCertificate); + const castVoteRecordJson = JSON.stringify(castVoteRecord); + const signature = await auth.generateSignature( + Buffer.from(castVoteRecordJson, 'utf-8'), + { pin: input.pin } + ); + assert(signature); + + const pdf = await mailingLabel.buildPdf(); + + if (!process.env.MAILING_LABEL_PRINTER) { + throw new Error('MAILING_LABEL_PRINTER not set'); + } + + execFileSync( + 'lpr', + ['-P', process.env.MAILING_LABEL_PRINTER, '-o', 'media=Custom.4x6in'], + { input: pdf } + ); + + return workspace.store.createCastBallot({ + id: ballotId, + registrationId: registration.clientId, + commonAccessCardCertificate, + castVoteRecord: Buffer.from(castVoteRecordJson), + castVoteRecordSignature: signature, + }); + }, + + async sync() { + const authStatus = await getAuthStatus(); + assert( + authStatus.status === 'has_card' && authStatus.isAdmin, + 'not logged in as admin' + ); + + void raveServerClient.sync({ authStatus }); + }, + + getServerSyncStatus() { + return { + attempts: workspace.store.getServerSyncAttempts(), + status: workspace.store.getSyncStatus(), + }; + }, + + logOut() { + return auth.logOut(); + }, + + createTestVoter(input: CreateTestVoterInput) { + assertIsIntegrationTest(); + + function createUniqueCommonAccessCardId(): Id { + const tenRandomDigits = Math.floor(Math.random() * 1e10).toString(); + return `test-${tenRandomDigits.toString().padStart(10, '0')}`; + } + + const commonAccessCardId = createUniqueCommonAccessCardId(); + if (input.isAdmin) { + workspace.store.createAdmin({ + machineId: VX_MACHINE_ID, + commonAccessCardId, + }); + } + + if (input.registrationRequest || input.registration) { + const registrationRequestId = ClientId(); + + workspace.store.createRegistrationRequest({ + id: registrationRequestId, + commonAccessCardId, + givenName: input.registrationRequest?.givenName ?? 'Rebecca', + familyName: input.registrationRequest?.familyName ?? 'Welton', + addressLine1: + input.registrationRequest?.addressLine1 ?? '123 Main St', + addressLine2: input.registrationRequest?.addressLine2, + city: input.registrationRequest?.city ?? 'Anytown', + state: input.registrationRequest?.state ?? 'CA', + postalCode: input.registrationRequest?.postalCode ?? '95959', + stateId: input.registrationRequest?.stateId ?? 'B2201793', + }); + + if (input.registration?.electionData) { + const electionId = ClientId(); + workspace.store.createElection({ + id: electionId, + definition: Buffer.from(input.registration.electionData), + }); + const electionRecord = workspace.store.getElection({ + clientId: electionId, + }); + assert(electionRecord); + + const { registration } = input; + const ballotStyle = registration.ballotStyleId + ? find( + electionRecord.electionDefinition.election.ballotStyles, + ({ id }) => id === registration.ballotStyleId + ) + : electionRecord.electionDefinition.election.ballotStyles[0]; + assert(ballotStyle); + + const precinctId = registration.precinctId + ? find( + ballotStyle.precincts, + (id) => id === registration.precinctId + ) + : ballotStyle.precincts[0]; + assert(typeof precinctId === 'string'); + + workspace.store.createRegistration({ + id: ClientId(), + registrationRequestId, + electionId, + precinctId, + ballotStyleId: ballotStyle.id, + }); + } + } + + return { commonAccessCardId }; + }, + + async getTestVoterCastVoteRecord() { + assertIsIntegrationTest(); + + const authStatus = await getAuthStatus(); + + if (authStatus.status !== 'has_card') { + throw new Error('Not logged in'); + } + + const { commonAccessCardId } = authStatus.card; + const mostRecentVotes = iter( + workspace.store.getRegistrations(commonAccessCardId) + ) + .flatMap((registration) => { + const selection = + workspace.store.getPrintedBallotCastVoteRecordForRegistration( + registration.id + ); + return selection ? [selection] : []; + }) + .first(); + + if (!mostRecentVotes) { + throw new Error('No votes found'); + } + + return mostRecentVotes; + }, + + /** + * For testing purposes only. + * + * ```sh + * curl -d '{}' -H 'Content-Type: application/json' http://localhost:3000/api/createMailingLabel \ + * | jq -r '.__grout_value' \ + * | base64 -d \ + * > /tmp/label.pdf + * ``` + */ + async createMailingLabel() { + return await mailingLabel.buildPdf(); + }, + }); } export type Api = ReturnType; -export function buildApp(): Application { +export function buildApp({ + auth, + workspace, + raveServerClient, +}: { + auth: Auth; + workspace: Workspace; + raveServerClient: RaveServerClient; +}): Application { const app: Application = express(); - const api = buildApi(); + const api = buildApi({ auth, workspace, raveServerClient }); + + app.use('/api/watchAuthStatus', (req, res) => { + res.set({ + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }); + + let timeout: NodeJS.Timeout | undefined; + let lastAuthStatus: AuthStatus | undefined; + + async function sendUpdate() { + const authStatus = await api.getAuthStatus(); + + if (!isDeepStrictEqual(authStatus, lastAuthStatus)) { + lastAuthStatus = authStatus; + res.write(`data: ${grout.serialize(authStatus)}\n\n`); + } + + timeout = setTimeout( + sendUpdate, + 10 /* AUTH_STATUS_POLLING_INTERVAL_MS */ + ); + } + + req.on('close', () => { + clearTimeout(timeout); + res.end(); + }); + + void sendUpdate(); + }); + app.use('/api', grout.buildRouter(api, express)); - useDevDockRouter(app, express); return app; } diff --git a/apps/rave-mark/backend/src/env.d.ts b/apps/rave-mark/backend/src/env.d.ts index 7d5c60dbff..fb4ed9add3 100644 --- a/apps/rave-mark/backend/src/env.d.ts +++ b/apps/rave-mark/backend/src/env.d.ts @@ -2,10 +2,14 @@ declare namespace NodeJS { export interface ProcessEnv { readonly CI?: string; readonly NODE_ENV: 'development' | 'production' | 'test'; + readonly IS_INTEGRATION_TEST?: string; + readonly USE_MOCK_RAVE_SERVER?: string; + readonly BASE_PORT?: string; readonly PORT?: string; - readonly VX_MACHINE_ID?: string; readonly VX_CODE_VERSION?: string; readonly VX_SCREEN_ORIENTATION?: 'portrait' | 'landscape'; - readonly MARK_WORKSPACE?: string; + readonly RAVE_MARK_WORKSPACE?: string; + readonly RAVE_URL?: string; + readonly MAILING_LABEL_PRINTER?: string; } } diff --git a/apps/rave-mark/backend/src/globals.ts b/apps/rave-mark/backend/src/globals.ts index 21281c7c1d..f08e844b06 100644 --- a/apps/rave-mark/backend/src/globals.ts +++ b/apps/rave-mark/backend/src/globals.ts @@ -1,22 +1,40 @@ -import { unsafeParse } from '@votingworks/types'; -import { z } from 'zod'; +import { NODE_ENV } from '@votingworks/backend'; +import { safeParseInt } from '@votingworks/types'; +import { join } from 'path'; + +const BASE_PORT = safeParseInt(process.env.BASE_PORT).ok() || 3000; /** - * Default port for the VxMark API. + * Default port for the RaveMark backend. */ -// eslint-disable-next-line vx/gts-safe-number-parse -export const PORT = Number(process.env.PORT || 3002); +export const PORT = safeParseInt(process.env.PORT).ok() || BASE_PORT + 2; -const NodeEnvSchema = z.union([ - z.literal('development'), - z.literal('test'), - z.literal('production'), -]); +/** + * Where should the database and audio files go? + */ +export const RAVE_MARK_WORKSPACE = + process.env.RAVE_MARK_WORKSPACE ?? + (NODE_ENV === 'development' + ? join(__dirname, '../dev-workspace') + : undefined); + +/** + * Is this running as part of an integration test? + */ +export const IS_INTEGRATION_TEST = process.env.IS_INTEGRATION_TEST === 'true'; + +/** + * RAVE Server URL. + */ +export const RAVE_URL = process.env.RAVE_URL + ? new URL(process.env.RAVE_URL) + : NODE_ENV === 'development' || typeof jest !== 'undefined' + ? new URL('http://localhost:8000') + : undefined; /** - * Which node environment is this? + * Should we mock the RAVE Server? */ -export const NODE_ENV = unsafeParse( - NodeEnvSchema, - process.env.NODE_ENV ?? 'development' -); +export const USE_MOCK_RAVE_SERVER = + (process.env.USE_MOCK_RAVE_SERVER === 'true' || IS_INTEGRATION_TEST) && + process.env.USE_MOCK_RAVE_SERVER !== 'false'; diff --git a/apps/rave-mark/backend/src/index.ts b/apps/rave-mark/backend/src/index.ts index a628524e24..44491499bd 100644 --- a/apps/rave-mark/backend/src/index.ts +++ b/apps/rave-mark/backend/src/index.ts @@ -1,11 +1,14 @@ +import { NODE_ENV } from '@votingworks/backend'; import { Logger, LogSource, LogEventId } from '@votingworks/logging'; import fs from 'fs'; import * as dotenv from 'dotenv'; import * as dotenvExpand from 'dotenv-expand'; import * as server from './server'; -import { NODE_ENV, PORT } from './globals'; +import { PORT, RAVE_MARK_WORKSPACE } from './globals'; +import { Workspace, createWorkspace } from './workspace'; -export type { Api } from './app'; +export type { AuthStatus } from './types/auth'; +export type { Api, CreateTestVoterInput } from './app'; // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use const dotEnvPath = '.env'; @@ -34,8 +37,27 @@ for (const dotenvFile of dotenvFiles) { const logger = new Logger(LogSource.VxMarkBackend); +function resolveWorkspace(): Workspace { + const workspacePath = RAVE_MARK_WORKSPACE; + if (!workspacePath) { + void logger.log(LogEventId.ScanServiceConfigurationMessage, 'system', { + message: + 'workspace path could not be determined; pass a workspace or run with MARK_WORKSPACE', + disposition: 'failure', + }); + throw new Error( + 'workspace path could not be determined; pass a workspace or run with MARK_WORKSPACE' + ); + } + return createWorkspace(workspacePath); +} + function main(): number { - server.start({ port: PORT, logger }); + server.start({ + port: PORT, + logger, + workspace: resolveWorkspace(), + }); return 0; } diff --git a/apps/rave-mark/backend/src/mailing_label.ts b/apps/rave-mark/backend/src/mailing_label.ts new file mode 100644 index 0000000000..8ada845867 --- /dev/null +++ b/apps/rave-mark/backend/src/mailing_label.ts @@ -0,0 +1,386 @@ +import { Buffer } from 'buffer'; +import puppeteer from 'puppeteer'; +import { dirSync } from 'tmp'; +import xml, { XmlAttrs, XmlObject } from 'xml'; + +export const SIZE_INCHES = { + width: 4, + height: 6, +} as const; + +export const SIZE_POINTS = { + width: SIZE_INCHES.width * 96, + height: SIZE_INCHES.height * 96, +} as const; + +function svg(attributes: XmlAttrs = {}, ...children: XmlObject[]): XmlObject { + return { svg: [{ _attr: attributes }, ...children] }; +} + +function g(attributes: XmlAttrs = {}, ...children: XmlObject[]): XmlObject { + return { g: [{ _attr: attributes }, ...children] }; +} + +function offset( + { x, y }: { x: number; y: number }, + ...children: XmlObject[] +): XmlObject { + return g({ transform: `translate(${x}, ${y})` }, ...children); +} + +function rect(attributes: XmlAttrs = {}): XmlObject { + return { rect: [{ _attr: attributes }] }; +} + +function line(attributes: XmlAttrs = {}): XmlObject { + return { line: [{ _attr: attributes }] }; +} + +function text(value: string, attributes: XmlAttrs): XmlObject { + return { text: [{ _attr: attributes }, value] }; +} + +function trackingNumberBarcode({ + width, + height, +}: { + width: number; + height: number; +}): XmlObject { + const lines: XmlObject[] = []; + + let bias = 0; + + for (let i = 0; i < width * 2; i += 1) { + if (Math.random() < 0.5 - bias) { + lines.push( + line({ + x1: i, + y1: 0, + x2: i, + y2: height, + stroke: 'black', + 'stroke-width': 1, + }) + ); + bias += 0.1; + } else { + bias -= 0.1; + } + } + + return g({}, ...lines); +} + +export function buildSvg2(): string { + return xml({}); +} + +export function buildSvg(): string { + const padding = { + x: 5.76, + y: 12.48, + } as const; + const inner = { + width: SIZE_POINTS.width - padding.x * 2, + height: SIZE_POINTS.height - padding.y * 2, + } as const; + const thickBorderSize = 4; + const mediumBorderSize = 3; + + return xml( + svg( + SIZE_POINTS, + offset( + padding, + svg( + inner, + rect({ + x: 2, + y: 2, + width: inner.width - thickBorderSize, + height: inner.height - thickBorderSize, + fill: 'white', + stroke: 'black', + 'stroke-width': thickBorderSize, + }), + line({ + x1: 0, + y1: 83.52, + x2: inner.width - thickBorderSize, + y2: 83.52, + stroke: 'black', + 'stroke-width': mediumBorderSize, + }), + line({ + x1: 0, + y1: '4.5in', + x2: inner.width - thickBorderSize, + y2: '4.5in', + stroke: 'black', + 'stroke-width': mediumBorderSize, + }), + offset( + { x: 1, y: 2 }, + svg( + { width: 63, height: 80 }, + text('E', { + x: '50%', + y: '73', + 'text-anchor': 'middle', + 'font-size': 92, + 'font-family': 'open-sans, sans-serif', + fill: 'white', + stroke: 'black', + 'stroke-width': mediumBorderSize, + }) + ) + ), + + // Official Election Mail + offset( + { x: 64, y: 0 }, + line({ + x1: 0, + y1: 0, + x2: 0, + y2: 83.52, + stroke: 'black', + 'stroke-width': mediumBorderSize, + }), + offset( + { x: 9.6, y: 0 }, + text('Official', { + x: 0, + y: 22.08, + 'dominant-baseline': 'middle', + 'font-size': 24, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('Election', { + x: 0, + y: 45.12, + 'dominant-baseline': 'middle', + 'font-size': 24, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('Mail', { + x: 0, + y: 68.16, + 'dominant-baseline': 'middle', + 'font-size': 24, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }) + ) + ), + + // QR Code + offset( + { x: 290, y: 8 }, + svg( + { width: 80, height: 70 }, + { + path: { + _attr: { + d: 'm 16,16 0,16 0,16 0,16 0,16 0,16 0,16 0,16 16,0 16,0 16,0 16,0 16,0 16,0 16,0 0,-16 0,-16 0,-16 0,-16 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 -16,0 -16,0 -16,0 -16,0 z m 128,0 0,16 0,16 16,0 0,-16 16,0 0,-16 -16,0 -16,0 z m 32,16 0,16 16,0 0,-16 -16,0 z m 16,16 0,16 16,0 16,0 0,-16 0,-16 0,-16 -16,0 0,16 0,16 -16,0 z m 0,16 -16,0 -16,0 -16,0 0,16 16,0 16,0 0,16 -16,0 0,16 16,0 0,16 16,0 0,-16 16,0 0,16 -16,0 0,16 -16,0 0,16 16,0 16,0 0,16 16,0 0,-16 0,-16 0,-16 0,-16 0,-16 -16,0 0,-16 -16,0 0,-16 z m 16,112 -16,0 0,16 -16,0 0,16 0,16 16,0 0,16 0,16 -16,0 -16,0 0,-16 16,0 0,-16 -16,0 0,-16 0,-16 -16,0 0,-16 16,0 0,16 16,0 0,-16 0,-16 -16,0 -16,0 0,-16 -16,0 -16,0 -16,0 0,16 -16,0 0,16 -16,0 0,-16 16,0 0,-16 -16,0 -16,0 0,16 -16,0 0,16 0,16 0,16 16,0 0,-16 16,0 16,0 16,0 0,-16 16,0 0,-16 16,0 0,16 -16,0 0,16 16,0 0,16 -16,0 0,16 16,0 16,0 0,16 0,16 0,16 16,0 0,16 16,0 16,0 16,0 0,16 -16,0 -16,0 -16,0 0,-16 -16,0 0,16 0,16 16,0 0,16 -16,0 0,16 16,0 16,0 0,-16 16,0 0,16 16,0 16,0 16,0 16,0 0,-16 16,0 0,16 16,0 16,0 16,0 0,-16 -16,0 -16,0 0,-16 -16,0 0,-16 -16,0 0,16 -16,0 -16,0 0,16 -16,0 0,-16 16,0 0,-16 0,-16 0,-16 16,0 0,-16 -16,0 -16,0 0,-16 16,0 0,-16 0,-16 0,-16 -16,0 0,-16 z m 48,128 0,-16 -16,0 0,16 16,0 z m 32,16 16,0 16,0 0,-16 -16,0 -16,0 0,16 z m 32,-16 16,0 0,-16 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 0,-16 -16,0 0,16 -16,0 0,16 0,16 16,0 0,-16 16,0 0,16 0,16 16,0 16,0 0,16 z m -48,-80 0,-16 -16,0 -16,0 0,16 16,0 16,0 z m 16,0 16,0 0,-16 0,-16 0,-16 16,0 0,16 16,0 0,16 16,0 0,-16 0,-16 -16,0 0,-16 16,0 0,-16 -16,0 -16,0 0,16 -16,0 0,-16 -16,0 0,16 -16,0 0,16 16,0 0,16 0,16 0,16 z m -16,-48 -16,0 0,16 16,0 0,-16 z m 64,32 -16,0 0,16 16,0 0,-16 z m -224,0 0,-16 -16,0 0,16 16,0 z m -16,0 -16,0 -16,0 -16,0 0,16 16,0 16,0 16,0 0,-16 z m -64,0 -16,0 0,16 16,0 0,-16 z m 0,-48 0,-16 -16,0 0,16 16,0 z m 112,-16 16,0 0,-16 0,-16 -16,0 0,16 0,16 z m 96,-128 0,16 0,16 0,16 0,16 0,16 0,16 0,16 16,0 16,0 16,0 16,0 16,0 16,0 16,0 0,-16 0,-16 0,-16 0,-16 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 -16,0 -16,0 -16,0 -16,0 z m -208,16 16,0 16,0 16,0 16,0 16,0 0,16 0,16 0,16 0,16 0,16 -16,0 -16,0 -16,0 -16,0 -16,0 0,-16 0,-16 0,-16 0,-16 0,-16 z m 224,0 16,0 16,0 16,0 16,0 16,0 0,16 0,16 0,16 0,16 0,16 -16,0 -16,0 -16,0 -16,0 -16,0 0,-16 0,-16 0,-16 0,-16 0,-16 z m -208,16 0,16 0,16 0,16 16,0 16,0 16,0 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 z m 224,0 0,16 0,16 0,16 16,0 16,0 16,0 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 z m -32,96 0,16 16,0 0,-16 -16,0 z m -224,96 0,16 0,16 0,16 0,16 0,16 0,16 0,16 16,0 16,0 16,0 16,0 16,0 16,0 16,0 0,-16 0,-16 0,-16 0,-16 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 -16,0 -16,0 -16,0 -16,0 z m 16,16 16,0 16,0 16,0 16,0 16,0 0,16 0,16 0,16 0,16 0,16 -16,0 -16,0 -16,0 -16,0 -16,0 0,-16 0,-16 0,-16 0,-16 0,-16 z m 16,16 0,16 0,16 0,16 16,0 16,0 16,0 0,-16 0,-16 0,-16 -16,0 -16,0 -16,0 z m 288,48 0,16 16,0 0,-16 -16,0 z', + transform: 'scale(0.20)', + style: 'fill: #000000; stroke: none;', + }, + }, + } + ) + ), + + // USPS Priority Mail + offset( + { x: 0, y: 84 }, + svg( + { width: inner.width, height: 32 }, + text('USPS PRIORITY MAIL®', { + x: '50%', + y: '50%', + 'dominant-baseline': 'middle', + 'text-anchor': 'middle', + 'font-size': 20, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + 'font-weight': 600, + }), + line({ + x1: 0, + y1: 28, + x2: '100%', + y2: 28, + stroke: 'black', + 'stroke-width': mediumBorderSize, + }) + ) + ), + + // Return Address + offset( + { x: 12, y: 84 + 35 }, + svg( + { width: inner.width, height: 96 }, + text('Jane Doe', { + x: 0, + y: 0, + 'dominant-baseline': 'hanging', + 'font-size': 10, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('Example Military Base', { + x: 0, + y: 11, + 'dominant-baseline': 'hanging', + 'font-size': 10, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('1234 Main St', { + x: 0, + y: 22, + 'dominant-baseline': 'hanging', + 'font-size': 10, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('Anytown, CA 95959', { + x: 0, + y: 33, + 'dominant-baseline': 'hanging', + 'font-size': 10, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }) + ) + ), + + // Shipping Address + offset( + { x: 12, y: 84 + 35 + 39 + 13 + 55 }, + svg( + { width: inner.width, height: 96 }, + text('Ship', { + x: 0, + y: 0, + 'dominant-baseline': 'hanging', + 'font-size': 12, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('to:', { + x: 0, + y: 11, + 'dominant-baseline': 'hanging', + 'font-size': 12, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('Ballot Receiving Center', { + x: 55, + y: 0, + 'dominant-baseline': 'hanging', + 'font-size': 14, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('1234 Main St', { + x: 55, + y: 16, + 'dominant-baseline': 'hanging', + 'font-size': 14, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + text('Anytown, CA 95959', { + x: 55, + y: 32, + 'dominant-baseline': 'hanging', + 'font-size': 14, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }) + ) + ), + + // Tracking Number + offset( + { x: inner.width / 20, y: 441.6 }, + svg( + { width: inner.width - inner.width / 10, height: 96 }, + text('USPS Tracking #', { + x: '50%', + y: 0, + 'dominant-baseline': 'hanging', + 'text-anchor': 'middle', + 'font-size': 10, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }), + offset( + { x: 0, y: 12 }, + trackingNumberBarcode({ + width: inner.width - inner.width / 10, + height: 72, + }), + text('9400 1000 0000 0000 0000 00', { + x: '50%', + y: 76, + 'dominant-baseline': 'hanging', + 'text-anchor': 'middle', + 'font-size': 10, + 'font-family': 'open-sans, sans-serif', + fill: 'black', + style: 'text-transform: uppercase;', + }) + ) + ) + ) + ) + ) + ), + { declaration: true, indent: ' ' } + ).toString(); +} + +export async function buildPdf(): Promise { + const content = buildSvg(); + const userDataDirTemp = dirSync({ unsafeCleanup: true }); + const browser = await puppeteer.launch({ + executablePath: '/usr/bin/chromium', + headless: 'new', + userDataDir: userDataDirTemp.name, + }); + + const page = await browser.newPage(); + await page.setContent(content); + + const pdf = await page.pdf({ + width: `${SIZE_INCHES.width}in`, + height: `${SIZE_INCHES.height}in`, + printBackground: true, + }); + + await browser.close(); + userDataDirTemp.removeCallback(); + + return pdf; +} diff --git a/apps/rave-mark/backend/src/rave_server_client.ts b/apps/rave-mark/backend/src/rave_server_client.ts new file mode 100644 index 0000000000..67b7e5ac94 --- /dev/null +++ b/apps/rave-mark/backend/src/rave_server_client.ts @@ -0,0 +1,344 @@ +/* eslint-disable max-classes-per-file */ + +import { VX_MACHINE_ID } from '@votingworks/backend'; +import { assert } from '@votingworks/basics'; +import { unsafeParse } from '@votingworks/types'; +import { format } from '@votingworks/utils'; +import { Buffer } from 'buffer'; +import fetch from 'cross-fetch'; +import makeDebug from 'debug'; +import { Store } from './store'; +import { AuthStatus } from './types/auth'; +import { ClientId } from './types/db'; +import { + RaveMarkSyncOutputSchema, + RaveServerSyncInput, + RaveServerSyncOutput, +} from './types/sync'; + +const debug = makeDebug('rave-server-client'); + +export interface RaveServerClient { + sync(options?: { authStatus: AuthStatus }): Promise; +} + +function describeSyncInputOrOutput( + data: RaveServerSyncInput | RaveServerSyncOutput +): string[] { + const parts: string[] = []; + + if (data.printedBallots?.length) { + parts.push( + format.countPhrase({ + value: data.printedBallots.length, + one: '1 printed ballot', + many: `${data.printedBallots.length} printed ballots`, + }) + ); + } + + if (data.scannedBallots?.length) { + parts.push( + format.countPhrase({ + value: data.scannedBallots.length, + one: '1 scanned ballot', + many: `${data.scannedBallots.length} scanned ballots`, + }) + ); + } + + if (data.elections?.length) { + parts.push( + format.countPhrase({ + value: data.elections.length, + one: '1 election', + many: `${data.elections.length} elections`, + }) + ); + } + + if (data.registrationRequests?.length) { + parts.push( + format.countPhrase({ + value: data.registrationRequests.length, + one: '1 registration request', + many: `${data.registrationRequests.length} registration requests`, + }) + ); + } + + if (data.registrations?.length) { + parts.push( + format.countPhrase({ + value: data.registrations.length, + one: '1 registration', + many: `${data.registrations.length} registrations`, + }) + ); + } + + return parts; +} + +export class RaveServerClientImpl { + private readonly store: Store; + private readonly baseUrl: URL; + + constructor({ store, baseUrl }: { store: Store; baseUrl: URL }) { + this.store = store; + this.baseUrl = baseUrl; + } + + async sync({ authStatus }: { authStatus?: AuthStatus } = {}): Promise { + const user = authStatus?.status === 'has_card' ? authStatus.card : null; + assert(!authStatus || user, 'not logged in as voter'); + const creator = user?.commonAccessCardId ?? 'system'; + + const syncAttemptId = this.store.createServerSyncAttempt({ + creator, + trigger: user ? 'manual' : 'scheduled', + initialStatusMessage: 'Syncing…', + }); + + try { + const input = this.createSyncInput(); + const output = await this.performSyncRequest(input); + this.updateLocalStoreFromSyncOutput(output); + this.updateServerSyncAttempt({ syncAttemptId, input, output }); + } catch (error) { + debug( + 'RAVE Server sync failed: %s', + error instanceof Error ? error.stack : error + ); + this.store.updateServerSyncAttempt({ + id: syncAttemptId, + status: 'failure', + statusMessage: + error instanceof Error ? error.message : `unknown error: ${error}`, + }); + } + } + + private createSyncInput(): RaveServerSyncInput { + const input: RaveServerSyncInput = { + lastSyncedRegistrationRequestId: + this.store.getLastSyncedRegistrationRequestId(), + lastSyncedRegistrationId: this.store.getLastSyncedRegistrationId(), + lastSyncedElectionId: this.store.getLastSyncedElectionId(), + lastSyncedPrintedBallotId: this.store.getLastSyncedPrintedBallotId(), + lastSyncedScannedBallotId: this.store.getLastSyncedScannedBallotId(), + registrationRequests: this.store.getRegistrationRequestsToSync(), + elections: this.store.getElectionsToSync(), + registrations: this.store.getRegistrationsToSync(), + printedBallots: this.store.getPrintedBallotsToSync(), + scannedBallots: this.store.getScannedBallotsToSync(), + }; + debug('RAVE sync input: %O', input); + return input; + } + + private async performSyncRequest( + input: RaveServerSyncInput + ): Promise { + const syncUrl = new URL('api/sync', this.baseUrl); + const response = await fetch(syncUrl.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(input), + }); + if (response.status !== 200) { + const text = await response.text(); + throw new Error( + `Server responded with ${response.status} ${response.statusText}: ${text}}` + ); + } + + return this.parseSyncOutput(await response.json()); + } + + private parseSyncOutput(rawOutput: unknown): RaveServerSyncOutput { + debug('RAVE sync raw output: %O', rawOutput); + const output = unsafeParse(RaveMarkSyncOutputSchema, rawOutput); + debug('RAVE sync parsed output: %O', output); + return output; + } + + private updateLocalStoreFromSyncOutput(output: RaveServerSyncOutput): void { + this.store.resetAdmins(); + for (const admin of output.admins) { + this.store.createAdmin(admin); + } + debug('reset and replaced admins; count: %d', output.admins.length); + + for (const election of output.elections) { + const electionId = this.store.createElection({ + id: + election.machineId === VX_MACHINE_ID ? election.clientId : ClientId(), + ...election, + definition: Buffer.from(election.definition, 'base64'), + }); + debug('created or replaced election %s', electionId); + } + + for (const registrationRequest of output.registrationRequests) { + const registrationRequestId = this.store.createRegistrationRequest({ + id: + registrationRequest.machineId === VX_MACHINE_ID + ? registrationRequest.clientId + : ClientId(), + ...registrationRequest, + }); + debug( + 'created or replaced registration request %s', + registrationRequestId + ); + } + + for (const registration of output.registrations) { + const localRegistrationRequest = this.store.getRegistrationRequest({ + serverId: registration.registrationRequestId, + }); + assert( + localRegistrationRequest, + `could not find local registration request with server id ${registration.registrationRequestId}` + ); + + const localElection = this.store.getElection({ + serverId: registration.electionId, + }); + assert( + localElection, + `could not find local election with server id ${registration.electionId}` + ); + + const registrationId = this.store.createRegistration({ + id: + registration.machineId === VX_MACHINE_ID + ? registration.clientId + : ClientId(), + serverId: registration.serverId, + clientId: registration.clientId, + machineId: registration.machineId, + registrationRequestId: localRegistrationRequest.id, + electionId: localElection.id, + ballotStyleId: registration.ballotStyleId, + precinctId: registration.precinctId, + }); + + debug('created or replaced registration %s', registrationId); + } + + for (const printedBallot of output.printedBallots) { + const localRegistration = this.store.getRegistration({ + serverId: printedBallot.registrationId, + }); + + assert( + localRegistration, + `could not find local registration with server id ${printedBallot.registrationId}` + ); + + const ballotId = this.store.createPrintedBallot({ + id: + printedBallot.machineId === VX_MACHINE_ID + ? printedBallot.clientId + : ClientId(), + serverId: printedBallot.serverId, + clientId: printedBallot.clientId, + machineId: printedBallot.machineId, + registrationId: localRegistration.clientId, + commonAccessCardCertificate: Buffer.from( + printedBallot.commonAccessCardCertificate, + 'base64' + ), + castVoteRecord: Buffer.from(printedBallot.castVoteRecord, 'base64'), + castVoteRecordSignature: Buffer.from( + printedBallot.castVoteRecordSignature, + 'base64' + ), + }); + + debug('created or replaced printed ballot %s', ballotId); + } + + for (const scannedBallot of output.scannedBallots) { + const localElection = this.store.getElection({ + serverId: scannedBallot.electionId, + }); + + assert( + localElection, + `could not find local election with server id ${scannedBallot.electionId}` + ); + + const ballotId = this.store.createScannedBallot({ + id: + scannedBallot.machineId === VX_MACHINE_ID + ? scannedBallot.clientId + : ClientId(), + serverId: scannedBallot.serverId, + clientId: scannedBallot.clientId, + machineId: scannedBallot.machineId, + electionId: localElection.id, + castVoteRecord: scannedBallot.castVoteRecord, + }); + + debug('created or replaced scanned ballot %s', ballotId); + } + } + + private updateServerSyncAttempt({ + syncAttemptId, + input, + output, + }: { + syncAttemptId: ClientId; + input: RaveServerSyncInput; + output: RaveServerSyncOutput; + }): void { + const sentParts = describeSyncInputOrOutput(input); + const receivedParts = describeSyncInputOrOutput(output); + + this.store.updateServerSyncAttempt({ + id: syncAttemptId, + status: 'success', + statusMessage: `SENT: ${ + sentParts.length === 0 ? 'nothing' : sentParts.join(', ') + }\nRECEIVED: ${ + receivedParts.length === 0 ? 'nothing' : receivedParts.join(', ') + }`, + }); + } +} + +export class MockRaveServerClient implements RaveServerClient { + constructor(private readonly store: Store) {} + + async sync({ + authStatus, + }: { + authStatus?: AuthStatus; + } = {}): Promise { + await Promise.resolve(); + + const card = authStatus?.status === 'has_card' ? authStatus.card : null; + assert(!authStatus || card, 'not logged in as voter'); + const creator = card?.commonAccessCardId ?? 'system'; + + const id = this.store.createServerSyncAttempt({ + creator, + trigger: card ? 'manual' : 'scheduled', + initialStatusMessage: 'Syncing…', + }); + + setTimeout(() => { + this.store.updateServerSyncAttempt({ + id, + status: 'success', + statusMessage: `SENT: nothing\nRECEIVED: nothing`, + }); + }, 1000); + } +} diff --git a/apps/rave-mark/backend/src/server.test.ts b/apps/rave-mark/backend/src/server.test.ts index 9b9ffbf5e7..100ae17094 100644 --- a/apps/rave-mark/backend/src/server.test.ts +++ b/apps/rave-mark/backend/src/server.test.ts @@ -1,13 +1,30 @@ import { fakeLogger } from '@votingworks/logging'; -import { PORT } from './globals'; +import { rmSync } from 'fs-extra'; +import { dirSync } from 'tmp'; import { start } from './server'; +import { createWorkspace } from './workspace'; +import { buildMockAuth } from '../test/app_helpers'; + +let workdir: string; + +beforeEach(() => { + workdir = dirSync().name; +}); + +afterEach(() => { + rmSync(workdir, { recursive: true }); + workdir = ''; +}); test('can start server', () => { const logger = fakeLogger(); + const auth = buildMockAuth(); const server = start({ + auth, logger, - port: PORT, + port: 0, + workspace: createWorkspace(workdir), }); expect(server.listening).toBeTruthy(); server.close(); diff --git a/apps/rave-mark/backend/src/server.ts b/apps/rave-mark/backend/src/server.ts index 9ef589adab..1d6d8488b0 100644 --- a/apps/rave-mark/backend/src/server.ts +++ b/apps/rave-mark/backend/src/server.ts @@ -1,24 +1,142 @@ +import { cac } from '@votingworks/auth'; +import { assertDefined, throwIllegalValue } from '@votingworks/basics'; import { LogEventId, Logger } from '@votingworks/logging'; import { Server } from 'http'; import { buildApp } from './app'; +import { RAVE_URL, USE_MOCK_RAVE_SERVER } from './globals'; +import { + MockRaveServerClient, + RaveServerClient, + RaveServerClientImpl, +} from './rave_server_client'; +import { Store } from './store'; +import { Auth } from './types/auth'; +import { Workspace } from './workspace'; export interface StartOptions { + auth?: Auth; + workspace: Workspace; logger: Logger; - port: number | string; + port: number; +} + +function getDefaultAuth(store: Store): Auth { + const card = new cac.CommonAccessCard(); + + return { + async checkPin(pin) { + const result = await card.checkPin(pin); + return result.response === 'correct'; + }, + + async getAuthStatus() { + const status = await card.getCardStatus(); + + switch (status.status) { + case 'no_card': + case 'unknown_error': + case 'card_error': + return { status: 'no_card' }; + + case 'ready': { + const cardDetails = assertDefined(status.cardDetails); + const isAdmin = store.isAdmin(cardDetails.commonAccessCardId); + return { + status: 'has_card', + card: cardDetails, + isAdmin, + }; + } + + /* istanbul ignore next: Compile-time check for completeness */ + default: + throwIllegalValue(status); + } + }, + + getCertificate() { + return card.getCertificate({ objectId: cac.CARD_DOD_CERT.OBJECT_ID }); + }, + + generateSignature(message, options) { + return card.generateSignature(message, { + privateKeyId: cac.CARD_DOD_CERT.PRIVATE_KEY_ID, + pin: options.pin, + }); + }, + + async logOut() { + await card.disconnect(); + }, + }; +} + +function getRaveServerClient(workspace: Workspace): RaveServerClient { + if (USE_MOCK_RAVE_SERVER) { + return new MockRaveServerClient(workspace.store); + } + const baseUrl = RAVE_URL; + + if (!baseUrl) { + throw new Error('RAVE_URL is not set'); + } + + return new RaveServerClientImpl({ + store: workspace.store, + baseUrl, + }); } /** * Starts the server with all the default options. */ -export function start({ logger, port }: StartOptions): Server { - const app = buildApp(); +export function start({ auth, logger, port, workspace }: StartOptions): Server { + const raveServerClient = getRaveServerClient(workspace); + const resolvedAuth = auth ?? getDefaultAuth(workspace.store); + const app = buildApp({ + workspace, + auth: resolvedAuth, + raveServerClient, + }); + + async function doRaveServerSync() { + try { + await raveServerClient.sync(); + + await logger.log(LogEventId.ApplicationStartup, 'system', { + message: 'RAVE Server sync succeeded', + disposition: 'success', + }); + } catch (err) { + await logger.log(LogEventId.ApplicationStartup, 'system', { + message: `Failed to sync with RAVE Server: ${err}`, + disposition: 'failure', + }); + } + + // run again in 5 seconds + setTimeout(doRaveServerSync, 1000 * 5); + } + + void doRaveServerSync().then( + () => + logger.log(LogEventId.ApplicationStartup, 'system', { + message: 'Started RAVE Server sync', + disposition: 'success', + }), + (err) => + logger.log(LogEventId.ApplicationStartup, 'system', { + message: `Failed to start RAVE Server sync: ${err}`, + disposition: 'failure', + }) + ); return app.listen( port, /* istanbul ignore next */ async () => { await logger.log(LogEventId.ApplicationStartup, 'system', { - message: `VxMark backend running at http://localhost:${port}/`, + message: `RaveMark backend running at http://localhost:${port}/`, disposition: 'success', }); } diff --git a/apps/rave-mark/backend/src/store.test.ts b/apps/rave-mark/backend/src/store.test.ts new file mode 100644 index 0000000000..de6706d60c --- /dev/null +++ b/apps/rave-mark/backend/src/store.test.ts @@ -0,0 +1,73 @@ +import { Buffer } from 'buffer'; +import { electionTwoPartyPrimaryFixtures } from '@votingworks/fixtures'; +import { + DEFAULT_SYSTEM_SETTINGS, + SystemSettings, + safeParseSystemSettings, +} from '@votingworks/types'; +import { Store } from './store'; +import { ClientId } from './types/db'; + +// We pause in some of these tests so we need to increase the timeout +jest.setTimeout(20000); + +test('getDbPath', () => { + const store = Store.memoryStore(); + expect(store.getDbPath()).toEqual(':memory:'); +}); + +test('get/set/delete system settings', () => { + const store = Store.memoryStore(); + + expect(store.getSystemSettings()).toBeUndefined(); + const systemSettings = safeParseSystemSettings( + electionTwoPartyPrimaryFixtures.systemSettings.asText() + ).unsafeUnwrap(); + + store.setSystemSettings(systemSettings); + expect(store.getSystemSettings()).toEqual(systemSettings); + + store.deleteSystemSettings(); + expect(store.getSystemSettings()).toBeUndefined(); +}); + +test('setSystemSettings can handle boolean values in input', () => { + const store = Store.memoryStore(); + const systemSettingsWithTrue: SystemSettings = { + ...DEFAULT_SYSTEM_SETTINGS, + auth: { + ...DEFAULT_SYSTEM_SETTINGS.auth, + arePollWorkerCardPinsEnabled: true, + }, + }; + + store.setSystemSettings(systemSettingsWithTrue); + let settings = store.getSystemSettings(); + expect(settings?.auth.arePollWorkerCardPinsEnabled).toEqual(true); + + store.reset(); + const systemSettingsWithFalse: SystemSettings = { + ...systemSettingsWithTrue, + auth: { + ...systemSettingsWithTrue.auth, + arePollWorkerCardPinsEnabled: false, + }, + }; + store.setSystemSettings(systemSettingsWithFalse); + settings = store.getSystemSettings(); + expect(settings?.auth.arePollWorkerCardPinsEnabled).toEqual(false); +}); + +test('reset clears the database', () => { + const { electionDefinition } = electionTwoPartyPrimaryFixtures; + const store = Store.memoryStore(); + + const electionId = ClientId(); + store.createElection({ + id: electionId, + definition: Buffer.from(electionDefinition.electionData), + }); + expect(store.getElection({ clientId: electionId })).toBeTruthy(); + store.reset(); + expect(store.getElection({ clientId: electionId })).toBeFalsy(); +}); diff --git a/apps/rave-mark/backend/src/store.ts b/apps/rave-mark/backend/src/store.ts new file mode 100644 index 0000000000..0f126dfb1d --- /dev/null +++ b/apps/rave-mark/backend/src/store.ts @@ -0,0 +1,1100 @@ +import { VX_MACHINE_ID } from '@votingworks/backend'; +import { assert, Optional } from '@votingworks/basics'; +import { Client as DbClient } from '@votingworks/db'; +import { + BallotStyleId, + ElectionDefinition, + Id, + PrecinctId, + safeParseElectionDefinition, + safeParseSystemSettings, + SystemSettings, + unsafeParse, + VotesDict, +} from '@votingworks/types'; +import { Buffer } from 'buffer'; +import { DateTime } from 'luxon'; +import { join } from 'path'; +import { + PrintedBallotRow, + ClientId, + deserializePrintedBallot, + deserializeElection, + deserializeRegistration, + deserializeRegistrationRequest, + deserializeServerSyncAttempt, + Election, + ElectionRow, + Registration, + RegistrationRequest, + RegistrationRequestRow, + RegistrationRow, + ServerId, + ServerSyncAttempt, + ServerSyncAttemptRow, + ScannedBallotRow, + deserializeScannedBallot, +} from './types/db'; +import { + Base64StringSchema, + ElectionInput, + PrintedBallotInput, + ScannedBallotInput, +} from './types/sync'; + +const SchemaPath = join(__dirname, '../schema.sql'); + +/** + * Manages a data store for imported election definition and system settings + */ +export class Store { + private constructor(private readonly client: DbClient) {} + + getDbPath(): string { + return this.client.getDatabasePath(); + } + + /** + * Builds and returns a new store whose data is kept in memory. + */ + static memoryStore(): Store { + return new Store(DbClient.memoryClient(SchemaPath)); + } + + /** + * Builds and returns a new store at `dbPath`. + */ + static fileStore(dbPath: string): Store { + return new Store(DbClient.fileClient(dbPath, SchemaPath)); + } + + /** + * Resets the database and any cached data in the store. + */ + reset(): void { + this.client.reset(); + } + + /** + * Gets an election. + */ + getElection({ clientId }: { clientId: ClientId }): Optional; + getElection({ serverId }: { serverId: ServerId }): Optional; + getElection({ + clientId, + serverId, + }: { + clientId?: ClientId; + serverId?: ServerId; + }): Optional { + const id = clientId ?? serverId; + assert(id, 'Must provide either clientId or serverId'); + const electionRow = this.client.one( + `select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + definition + from elections + where ${clientId ? 'client_id' : 'server_id'} = ? + `, + id + ) as Optional; + + return electionRow ? deserializeElection(electionRow) : undefined; + } + + /** + * Creates a new election record. + */ + createElection({ + id, + serverId, + clientId, + machineId = VX_MACHINE_ID, + definition, + }: { + id: ClientId; + serverId?: ServerId; + clientId?: ClientId; + machineId?: Id; + definition: Buffer; + }): ClientId { + assert( + (serverId === undefined) === (clientId === undefined), + 'election serverId must be defined if and only if clientId is defined' + ); + assert( + (machineId === VX_MACHINE_ID) === (clientId === id || !clientId), + 'election machineId must be VX_MACHINE_ID if and only if ID equals clientId' + ); + + const electionData = definition.toString('utf-8'); + safeParseElectionDefinition(electionData).assertOk( + `Unable to parse election data: ${electionData}` + ); + + this.client.run( + ` + insert or replace into elections ( + id, + server_id, + client_id, + machine_id, + definition + ) values ( + ?, ?, ?, ?, ? + ) + `, + id, + serverId ?? null, + clientId ?? id, + machineId, + definition + ); + + return id; + } + + /** + * Deletes system settings + */ + deleteSystemSettings(): void { + this.client.run('delete from system_settings'); + } + + /** + * Creates a system settings record + */ + setSystemSettings(systemSettings: SystemSettings): void { + this.client.run('delete from system_settings'); + this.client.run( + `insert into system_settings (data) values (?)`, + JSON.stringify(systemSettings) + ); + } + + /** + * Gets system settings or undefined if they aren't loaded yet + */ + getSystemSettings(): SystemSettings | undefined { + const result = this.client.one( + `select data from system_settings` + ) as Optional<{ data: string }>; + + if (!result) { + return undefined; + } + return safeParseSystemSettings(result.data).unsafeUnwrap(); + } + + /** + * Gets basic information about a voter by CAC ID. + */ + isAdmin(commonAccessCardId: Id): boolean { + const result = this.client.one( + ` + select + count(*) as count + from admins + where machine_id = ? + and common_access_card_id = ? + `, + VX_MACHINE_ID, + commonAccessCardId + ) as { count: number }; + + return result.count > 0; + } + + /** + * Makes a user with the given CAC ID an admin. + */ + createAdmin({ + machineId, + commonAccessCardId, + createdAt, + }: { + machineId: string; + commonAccessCardId: Id; + createdAt?: DateTime; + }): void { + this.client.run( + ` + insert or replace into admins ( + machine_id, + common_access_card_id, + created_at + ) values ( + ?, ?, ? + ) + `, + machineId, + commonAccessCardId, + (createdAt ?? DateTime.utc()).toSQL() + ); + } + + /** + * Clears all admin users. + */ + resetAdmins(): void { + this.client.run('delete from admins'); + } + + /** + * Gets all the registrations for a given voter by CAC ID. + */ + getRegistrationRequests(commonAccessCardId: Id): RegistrationRequest[] { + const result = this.client.all( + ` + select + id as id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + given_name as givenName, + family_name as familyName, + address_line_1 as addressLine1, + address_line_2 as addressLine2, + city as city, + state as state, + postal_code as postalCode, + state_id as stateId, + created_at as createdAt + from registration_requests + where common_access_card_id = ? + `, + commonAccessCardId + ) as RegistrationRequestRow[]; + + return result.map(deserializeRegistrationRequest); + } + + /** + * @returns registrations sorted by most recent first + */ + getRegistrations(commonAccessCardId: Id): Registration[] { + const result = this.client.all( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + registration_request_id as registrationRequestId, + election_id as electionId, + precinct_id as precinctId, + ballot_style_id as ballotStyleId, + created_at as createdAt + from registrations + where common_access_card_id = ? + order by created_at desc + `, + commonAccessCardId + ) as RegistrationRow[]; + + return result.map(deserializeRegistration); + } + + /** + * Gets the election for the given voter registration ID. + */ + getRegistrationElection( + registrationId: ClientId + ): Optional { + const result = this.client.one( + ` + select + definition + from registrations + inner join elections on elections.id = registrations.election_id + where registrations.id = ? + `, + registrationId + ) as Optional<{ definition: string | Buffer }>; + + if (!result) { + return undefined; + } + + const electionDefinitionParseResult = safeParseElectionDefinition( + result.definition.toString() + ); + + if (electionDefinitionParseResult.isErr()) { + throw new Error('Unable to parse stored election data.'); + } + + return electionDefinitionParseResult.ok(); + } + + /** + * Creates a registration request for the voter with the given CAC ID. + */ + createRegistrationRequest({ + id, + serverId, + clientId, + machineId = VX_MACHINE_ID, + commonAccessCardId, + givenName, + familyName, + addressLine1, + addressLine2, + city, + state, + postalCode, + stateId, + }: { + id: ClientId; + serverId?: ServerId; + clientId?: ClientId; + machineId?: Id; + commonAccessCardId: Id; + givenName: string; + familyName: string; + addressLine1: string; + addressLine2?: string; + city: string; + state: string; + postalCode: string; + stateId: string; + }): ClientId { + assert( + (serverId === undefined) === (clientId === undefined), + 'registration request serverId must be defined if and only if clientId is defined' + ); + assert( + (machineId === VX_MACHINE_ID) === (clientId === id || !clientId), + 'registration request machineId must be VX_MACHINE_ID if and only if ID equals clientId' + ); + + this.client.run( + ` + insert or replace into registration_requests ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id + ) values ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + `, + id, + serverId ?? null, + clientId ?? id, + machineId, + commonAccessCardId, + givenName, + familyName, + addressLine1, + addressLine2 ?? null, + city, + state, + postalCode, + stateId + ); + + return id; + } + + /** + * Associates a registration with an election. + */ + createRegistration({ + id, + serverId, + clientId, + machineId = VX_MACHINE_ID, + registrationRequestId, + electionId, + precinctId, + ballotStyleId, + }: { + id: ClientId; + serverId?: ServerId; + clientId?: ClientId; + machineId?: Id; + registrationRequestId: ClientId; + electionId: ClientId; + precinctId: PrecinctId; + ballotStyleId: BallotStyleId; + }): ClientId { + assert( + (serverId === undefined) === (clientId === undefined), + 'registration serverId must be defined if and only if clientId is defined' + ); + assert( + (machineId === VX_MACHINE_ID) === (clientId === id || !clientId), + 'registration machineId must be VX_MACHINE_ID if and only if ID equals clientId' + ); + + const registrationRequest = this.getRegistrationRequest({ + clientId: registrationRequestId, + }); + assert( + registrationRequest, + `registration request ${registrationRequestId} not found` + ); + + this.client.run( + ` + insert or replace into registrations ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id + ) values ( + ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + `, + id, + serverId ?? null, + clientId ?? id, + machineId, + registrationRequest.commonAccessCardId, + registrationRequest.id, + electionId, + precinctId, + ballotStyleId + ); + + return id; + } + + getRegistrationRequest({ + serverId, + }: { + serverId: ServerId; + }): Optional; + getRegistrationRequest({ + clientId, + }: { + clientId: ClientId; + }): Optional; + getRegistrationRequest({ + serverId, + clientId, + }: { + serverId?: ServerId; + clientId?: ClientId; + }): Optional { + const id = serverId ?? clientId; + assert(id !== undefined, 'serverId or clientId must be defined'); + + const result = this.client.one( + ` + select + id as id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + given_name as givenName, + family_name as familyName, + address_line_1 as addressLine1, + address_line_2 as addressLine2, + city as city, + state as state, + postal_code as postalCode, + state_id as stateId, + created_at as createdAt + from registration_requests + where ${serverId ? 'server_id' : 'client_id'} = ? + `, + id + ) as Optional; + + return result ? deserializeRegistrationRequest(result) : undefined; + } + + /** + * Records a cast ballot for a voter registration. + */ + createPrintedBallot({ + id, + serverId, + clientId, + machineId = VX_MACHINE_ID, + registrationId, + commonAccessCardCertificate, + castVoteRecord, + castVoteRecordSignature, + }: { + id: ClientId; + serverId?: ServerId; + clientId?: ClientId; + machineId?: Id; + registrationId: ClientId; + commonAccessCardCertificate: Buffer; + castVoteRecord: Buffer; + castVoteRecordSignature: Buffer; + }): ClientId { + assert( + (serverId === undefined) === (clientId === undefined), + 'ballot serverId must be defined if and only if clientId is defined' + ); + assert( + (machineId === VX_MACHINE_ID) === (clientId === id || !clientId), + 'ballot machineId must be VX_MACHINE_ID if and only if ID equals clientId' + ); + + const registration = this.getRegistration({ clientId: registrationId }); + assert( + registration, + `no registration found with client ID ${registrationId}` + ); + + // TODO: validate votes against election definition + this.client.run( + ` + insert or replace into printed_ballots ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature + ) values ( + ?, ?, ?, ?, ?, ?, ?, ?, ? + ) + `, + id, + serverId ?? null, + clientId ?? id, + machineId, + registration.commonAccessCardId, + commonAccessCardCertificate, + registration.id, + castVoteRecord, + castVoteRecordSignature + ); + + return id; + } + + createScannedBallot({ + id, + serverId, + clientId, + machineId, + electionId, + castVoteRecord, + }: { + id: ClientId; + serverId: ServerId; + clientId: ClientId; + machineId: string; + electionId: ClientId; + castVoteRecord: Buffer; + }): ClientId { + assert( + (serverId === undefined) === (clientId === undefined), + 'ballot serverId must be defined if and only if clientId is defined' + ); + assert( + (machineId === VX_MACHINE_ID) === (clientId === id || !clientId), + 'ballot machineId must be VX_MACHINE_ID if and only if ID equals clientId' + ); + + // TODO: validate votes against election definition + this.client.run( + ` + insert or replace into scanned_ballots ( + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record + ) values ( + ?, ?, ?, ?, ?, ? + ) + `, + id, + serverId ?? null, + clientId ?? id, + machineId, + electionId, + castVoteRecord + ); + + return id; + } + + /** + * Records a cast ballot for a voter registration. + */ + createCastBallot({ + id, + registrationId, + commonAccessCardCertificate, + castVoteRecord, + castVoteRecordSignature, + }: { + id: ClientId; + registrationId: ClientId; + commonAccessCardCertificate: Buffer; + castVoteRecord: Buffer; + castVoteRecordSignature: Buffer; + }): ClientId { + const registration = this.getRegistration({ clientId: registrationId }); + assert( + registration, + `no registration found with client ID ${registrationId}` + ); + + // TODO: validate votes against election definition + this.client.run( + ` + insert or replace into printed_ballots ( + id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature + ) values ( + ?, ?, ?, ?, ?, ?, ?, ? + ) + `, + id, + id, + VX_MACHINE_ID, + registration.commonAccessCardId, + commonAccessCardCertificate, + registration.id, + castVoteRecord, + castVoteRecordSignature + ); + + return id; + } + + getRegistration({ + clientId, + }: { + clientId?: ClientId; + }): Optional; + getRegistration({ + serverId, + }: { + serverId?: ServerId; + }): Optional; + getRegistration({ + clientId, + serverId, + }: { + clientId?: ClientId; + serverId?: ServerId; + }): Optional { + const id = clientId ?? serverId; + assert(id !== undefined, 'clientId or serverId must be defined'); + + const result = this.client.one( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + registration_request_id as registrationRequestId, + election_id as electionId, + precinct_id as precinctId, + ballot_style_id as ballotStyleId, + created_at as createdAt + from registrations + where ${clientId ? 'client_id' : 'server_id'} = ? + `, + id + ) as Optional; + + return result ? deserializeRegistration(result) : undefined; + } + + /** + * Gets the voter selection for the given voter registration ID. + */ + getPrintedBallotCastVoteRecordForRegistration( + registrationId: ClientId + ): Optional { + const result = this.client.one( + ` + select + cast_vote_record as castVoteRecordJson + from printed_ballots + where registration_id = ? + order by created_at desc + `, + registrationId + ) as Optional<{ castVoteRecordJson: string }>; + + if (!result) { + return undefined; + } + + return JSON.parse(result.castVoteRecordJson); + } + + /** + * Gets the sync status, i.e. the count of the synced and pending items of + * various types. + */ + getSyncStatus(): { + printedBallots: { synced: number; pending: number }; + pendingRegistrations: { synced: number; pending: number }; + elections: { synced: number; pending: number }; + } { + const printedBallots = this.client.one( + ` + select + sum(case when server_id is not null then 1 else 0 end) as synced, + sum(case when server_id is null then 1 else 0 end) as pending + from printed_ballots + ` + ) as { synced: number; pending: number }; + + const pendingRegistrations = this.client.one( + ` + select + sum(case when server_id is not null then 1 else 0 end) as synced, + sum(case when server_id is null then 1 else 0 end) as pending + from registrations + ` + ) as { synced: number; pending: number }; + + const elections = this.client.one( + ` + select + sum(case when server_id is not null then 1 else 0 end) as synced, + sum(case when server_id is null then 1 else 0 end) as pending + from elections + ` + ) as { synced: number; pending: number }; + + return { + printedBallots, + pendingRegistrations, + elections, + }; + } + + /** + * Creates a new server synchronization attempt record. + */ + createServerSyncAttempt({ + creator, + trigger, + initialStatusMessage, + }: { + creator: string; + trigger: 'manual' | 'scheduled'; + initialStatusMessage: string; + }): ClientId { + const id = ClientId(); + this.client.run( + ` + insert into server_sync_attempts ( + id, + creator, + trigger, + status_message + ) values ( + ?, ?, ?, ? + ) + `, + id, + creator, + trigger, + initialStatusMessage + ); + return id; + } + + /** + * Updates the state for the given server synchronization attempt. + */ + updateServerSyncAttempt({ + id, + status, + statusMessage, + }: { + id: ClientId; + status: 'pending' | 'success' | 'failure'; + statusMessage: string; + }): void { + this.client.run( + ` + update server_sync_attempts + set + status_message = ?, + success = ?, + completed_at = ? + where id = ? + `, + statusMessage, + status === 'pending' ? null : status === 'success' ? 1 : 0, + status === 'pending' ? null : DateTime.utc().toSQL(), + id + ); + } + + /** + * Gets the most recent server synchronization attempts. + */ + getServerSyncAttempts({ + limit = 100, + }: { limit?: number } = {}): ServerSyncAttempt[] { + return ( + this.client.all( + ` + select + id, + creator, + trigger, + status_message as statusMessage, + success, + created_at as createdAt, + completed_at as completedAt + from server_sync_attempts + order by created_at desc + limit ? + `, + limit + ) as ServerSyncAttemptRow[] + ).map(deserializeServerSyncAttempt); + } + + getLastSyncedElectionId(): Optional { + const result = this.client.one( + ` + select + server_id as serverId + from elections + where server_id is not null + order by created_at desc + ` + ) as Optional<{ serverId: ServerId }>; + + return result ? result.serverId : undefined; + } + + getLastSyncedRegistrationRequestId(): Optional { + const result = this.client.one( + ` + select + server_id as serverId + from registration_requests + where server_id is not null + order by created_at desc + ` + ) as Optional<{ serverId: ServerId }>; + + return result ? result.serverId : undefined; + } + + getLastSyncedRegistrationId(): Optional { + const result = this.client.one( + ` + select + server_id as serverId + from registrations + where server_id is not null + order by created_at desc + ` + ) as Optional<{ serverId: ServerId }>; + + return result ? result.serverId : undefined; + } + + getLastSyncedPrintedBallotId(): Optional { + const result = this.client.one( + ` + select + server_id as serverId + from printed_ballots + where server_id is not null + order by created_at desc + ` + ) as Optional<{ serverId: ServerId }>; + + return result ? result.serverId : undefined; + } + + getLastSyncedScannedBallotId(): Optional { + const result = this.client.one( + ` + select + server_id as serverId + from scanned_ballots + where server_id is not null + order by created_at desc + ` + ) as Optional<{ serverId: ServerId }>; + + return result ? result.serverId : undefined; + } + + getRegistrationRequestsToSync(): RegistrationRequest[] { + const result = this.client.all( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + given_name as givenName, + family_name as familyName, + address_line_1 as addressLine1, + address_line_2 as addressLine2, + city, + state, + postal_code as postalCode, + state_id as stateId + from registration_requests + where server_id is null + ` + ) as RegistrationRequestRow[]; + + return result.map(deserializeRegistrationRequest); + } + + getRegistrationsToSync(): Registration[] { + const result = this.client.all( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + (select client_id from registration_requests where id = registration_request_id) as registrationRequestId, + (select client_id from elections where id = election_id) as electionId, + precinct_id as precinctId, + ballot_style_id as ballotStyleId + from registrations + where server_id is null + ` + ) as RegistrationRow[]; + + return result.map(deserializeRegistration); + } + + getElectionsToSync(): ElectionInput[] { + const result = this.client.all( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + definition + from elections + where server_id is null + ` + ) as ElectionRow[]; + + return result.map((row) => { + const record = deserializeElection(row); + return { + ...record, + definition: unsafeParse( + Base64StringSchema, + record.definition.toString('base64') + ), + }; + }); + } + + getPrintedBallotsToSync(): PrintedBallotInput[] { + const result = this.client.all( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + common_access_card_id as commonAccessCardId, + common_access_card_certificate as commonAccessCardCertificate, + (select client_id from registrations where id = registration_id) as registrationId, + cast_vote_record as castVoteRecord, + cast_vote_record_signature as castVoteRecordSignature, + created_at as createdAt + from printed_ballots + where server_id is null + ` + ) as PrintedBallotRow[]; + + return result.map((row) => { + const record = deserializePrintedBallot(row); + return { + ...record, + commonAccessCardCertificate: unsafeParse( + Base64StringSchema, + record.commonAccessCardCertificate.toString('base64') + ), + castVoteRecord: unsafeParse( + Base64StringSchema, + record.castVoteRecord.toString('base64') + ), + castVoteRecordSignature: unsafeParse( + Base64StringSchema, + record.castVoteRecordSignature.toString('base64') + ), + }; + }); + } + + getScannedBallotsToSync(): ScannedBallotInput[] { + const result = this.client.all( + ` + select + id, + server_id as serverId, + client_id as clientId, + machine_id as machineId, + (select client_id from elections where id = election_id) as electionId, + cast_vote_record as castVoteRecord, + created_at as createdAt + from scanned_ballots + where server_id is null + ` + ) as ScannedBallotRow[]; + + return result.map((row) => { + const record = deserializeScannedBallot(row); + return { + ...record, + castVoteRecord: unsafeParse( + Base64StringSchema, + record.castVoteRecord.toString('base64') + ), + }; + }); + } +} diff --git a/apps/rave-mark/backend/src/types/auth.ts b/apps/rave-mark/backend/src/types/auth.ts new file mode 100644 index 0000000000..69277e7f79 --- /dev/null +++ b/apps/rave-mark/backend/src/types/auth.ts @@ -0,0 +1,33 @@ +import { Buffer } from 'buffer'; +import { cac } from '@votingworks/auth'; + +export type AuthStatus = + | { status: 'no_card' } + | { status: 'has_card'; card: cac.CommonAccessCardDetails; isAdmin: boolean }; + +export interface Auth { + /** + * Gets the current auth status. + */ + getAuthStatus(): Promise; + + /** + * Checks the PIN for the current user. + */ + checkPin(pin: string): Promise; + + /** + * Signs a message with the current user's private key. + */ + generateSignature(message: Buffer, options: { pin: string }): Promise; + + /** + * Gets the certificate for the current user. + */ + getCertificate(): Promise; + + /** + * Log out the current user. + */ + logOut(): Promise; +} diff --git a/apps/rave-mark/backend/src/types/db.ts b/apps/rave-mark/backend/src/types/db.ts new file mode 100644 index 0000000000..2bb32228f0 --- /dev/null +++ b/apps/rave-mark/backend/src/types/db.ts @@ -0,0 +1,500 @@ +import { Buffer } from 'buffer'; +import { Optional } from '@votingworks/basics'; +import { + BallotStyleId, + ElectionDefinition, + Id, + IdSchema, + NewType, + PrecinctId, + safeParseElectionDefinition, +} from '@votingworks/types'; +import { v4 as uuid } from 'uuid'; +import { DateTime } from 'luxon'; +import { z } from 'zod'; + +/** + * `ServerId` is the ID on the RAVE Server, not the ID in this backend. + */ +export type ServerId = NewType; + +/** + * Creates a new `ServerId`. + */ +export function ServerId(): ServerId { + return uuid() as ServerId; +} + +/** + * `ClientId` is the ID on this backend, not the ID on the RAVE Server. + */ +export type ClientId = NewType; + +/** + * Creates a new `ClientId`. + */ +export function ClientId(): ClientId { + return uuid() as ClientId; +} + +/** + * Schema for {@link ServerId}. + */ +export const ServerIdSchema = IdSchema as z.ZodSchema; + +/** + * Schema for {@link ClientId}. + */ +export const ClientIdSchema = IdSchema as z.ZodSchema; + +export interface RegistrationRequest { + /** + * Database ID for a registration request record. + */ + id: ClientId; + + /** + * Server-side ID for this registration request record, if sync'ed. + */ + serverId?: ServerId; + + /** + * Client-side ID for this registration request record. + */ + clientId: ClientId; + + /** + * Machine ID for the machine that created this registration request record. + */ + machineId: Id; + + /** + * Common Access Card ID for the voter who created this registration request. + */ + commonAccessCardId: Id; + + /** + * Voter's given name, i.e. first name. + */ + givenName: string; + + /** + * Voter's family name, i.e. last name. + */ + familyName: string; + + /** + * Voter's address line 1. + */ + addressLine1: string; + + /** + * Voter's address line 2. + */ + addressLine2?: string; + + /** + * Voter's city. + */ + city: string; + + /** + * Voter's state. + */ + state: string; + + /** + * Voter's postal code. + */ + postalCode: string; + + /** + * State-issued ID number for the voter, e.g. driver's license number. + */ + stateId: string; + + /** + * Date and time when the voter registered for the election. + */ + createdAt: DateTime; +} + +export interface RegistrationRequestRow { + id: string; + serverId: string | null; + clientId: string; + machineId: string; + commonAccessCardId: string; + givenName: string; + familyName: string; + addressLine1: string; + addressLine2: string | null; + city: string; + state: string; + postalCode: string; + stateId: string; + createdAt: string; +} + +export interface Election { + /** + * Database ID for an election record. + */ + id: ClientId; + + /** + * Server-side ID for this election record, if sync'ed. + */ + serverId?: ServerId; + + /** + * Client-side ID for this election record. + */ + clientId: ClientId; + + /** + * Machine ID for the machine that created this election record. + */ + machineId: Id; + + /** + * Election data. + */ + definition: Buffer; + + /** + * Election definition. + */ + electionDefinition: ElectionDefinition; +} + +export interface ElectionRow { + id: string; + serverId: string | null; + clientId: string; + machineId: string; + definition: Buffer; +} + +export function deserializeElection(row: ElectionRow): Election { + return { + id: row.id as ClientId, + serverId: row.serverId ? (row.serverId as ServerId) : undefined, + clientId: row.clientId as ClientId, + machineId: row.machineId, + definition: row.definition, + electionDefinition: safeParseElectionDefinition( + row.definition.toString('utf-8') + ).unsafeUnwrap(), + }; +} + +export interface Registration { + /** + * Database ID for an election registration record. + */ + id: ClientId; + + /** + * Server-side ID for this registration record, if sync'ed. + */ + serverId?: ServerId; + + /** + * Client-side ID for this registration record. + */ + clientId: ClientId; + + /** + * Machine ID for the machine that created this registration record. + */ + machineId: Id; + + /** + * Common Access Card ID for the voter this registration belongs to. + */ + commonAccessCardId: Id; + + /** + * Database ID for the voter's registration request record. + */ + registrationRequestId: ClientId; + + /** + * Database ID for a the election record associated with this voter + * registration. + */ + electionId: ClientId; + + /** + * Precinct ID for the voter's precinct. + */ + precinctId: PrecinctId; + + /** + * Ballot style ID for the voter's ballot style. + */ + ballotStyleId: BallotStyleId; + + /** + * Date and time when the voter registered for the election. + */ + createdAt: DateTime; +} + +export interface RegistrationRow { + id: string; + serverId: string | null; + clientId: string; + machineId: string; + commonAccessCardId: string; + registrationRequestId: string; + electionId: string; + precinctId: string; + ballotStyleId: string; + createdAt: string; +} + +export interface PrintedBallot { + /** + * Database ID for a ballot record. + */ + id: ClientId; + + /** + * Server-side ID for this registration record, if sync'ed. + */ + serverId?: ServerId; + + /** + * Client-side ID for this registration record. + */ + clientId: ClientId; + + /** + * Machine ID for the machine that created this registration record. + */ + machineId: Id; + + /** + * Common Access Card ID for the voter who created this ballot. + */ + commonAccessCardId: Id; + + /** + * Common Access Card X509 certificate. + */ + commonAccessCardCertificate: Buffer; + + /** + * Database ID for the associated registration record. + */ + registrationId: ClientId; + + /** + * Votes cast by the voter. + */ + castVoteRecord: Buffer; + + /** + * Signature of the cast vote record. + */ + castVoteRecordSignature: Buffer; + + /** + * Date and time when the voter cast their votes. + */ + createdAt: DateTime; +} + +export interface ScannedBallotRow { + id: string; + serverId: string | null; + clientId: string; + machineId: string; + electionId: string; + castVoteRecord: Buffer; + createdAt: string; +} + +export function deserializeScannedBallot(row: ScannedBallotRow): ScannedBallot { + return { + id: row.id as ClientId, + serverId: (row.serverId ?? undefined) as Optional, + clientId: row.clientId as ClientId, + machineId: row.machineId, + electionId: row.electionId as ClientId, + castVoteRecord: row.castVoteRecord, + createdAt: DateTime.fromSQL(row.createdAt), + }; +} + +export interface ScannedBallot { + /** + * Database ID for a ballot record. + */ + id: ClientId; + + /** + * Server-side ID for this registration record, if sync'ed. + */ + serverId?: ServerId; + + /** + * Client-side ID for this registration record. + */ + clientId: ClientId; + + /** + * Machine ID for the machine that created this registration record. + */ + machineId: Id; + + /** + * Database ID for the associated election record. + */ + electionId: ClientId; + + /** + * Votes cast by the voter. + */ + castVoteRecord: Buffer; + + /** + * Date and time when the voter cast their votes. + */ + createdAt: DateTime; +} + +export interface PrintedBallotRow { + id: string; + serverId: string | null; + clientId: string; + machineId: string; + commonAccessCardId: string; + commonAccessCardCertificate: Buffer; + registrationId: string; + castVoteRecord: Buffer; + castVoteRecordSignature: Buffer; + createdAt: string; +} + +export function deserializePrintedBallot(row: PrintedBallotRow): PrintedBallot { + return { + id: row.id as ClientId, + serverId: (row.serverId ?? undefined) as Optional, + clientId: row.clientId as ClientId, + machineId: row.machineId, + // because these are just strings of digits, sqlite may return them as + // numbers, so we have to convert them back to strings + commonAccessCardId: row.commonAccessCardId.toString(), + commonAccessCardCertificate: row.commonAccessCardCertificate, + registrationId: row.registrationId as ClientId, + castVoteRecord: row.castVoteRecord, + castVoteRecordSignature: row.castVoteRecordSignature, + createdAt: DateTime.fromSQL(row.createdAt), + }; +} + +export interface ServerSyncAttempt { + /** + * Database ID for a server sync attempt record. + */ + id: ClientId; + + /** + * Creator for the user who initiated the server sync attempt. + */ + creator: string; + + /** + * Trigger type for the server sync attempt. + */ + trigger: string; + + /** + * Status message for the server sync attempt. + */ + statusMessage: string; + + /** + * Date and time when the server sync attempt was made. + */ + createdAt: DateTime; + + /** + * Whether or not the server sync attempt was successful. + */ + success?: boolean; + + /** + * Date and time when the server sync attempt was completed. + */ + completedAt?: DateTime; +} + +export interface ServerSyncAttemptRow { + id: string; + creator: string; + trigger: string; + statusMessage: string; + success: 0 | 1 | null; + createdAt: string; + completedAt: string | null; +} + +export function deserializeRegistrationRequest( + row: RegistrationRequestRow +): RegistrationRequest { + return { + id: row.id as ClientId, + serverId: (row.serverId ?? undefined) as Optional, + clientId: row.clientId as ClientId, + machineId: row.machineId, + // because these are just strings of digits, sqlite may return them as + // numbers, so we have to convert them back to strings + commonAccessCardId: row.commonAccessCardId.toString(), + givenName: row.givenName, + familyName: row.familyName, + addressLine1: row.addressLine1, + addressLine2: row.addressLine2 ?? undefined, + city: row.city, + state: row.state, + postalCode: row.postalCode, + stateId: row.stateId, + createdAt: DateTime.fromSQL(row.createdAt), + }; +} + +export function deserializeRegistration(row: RegistrationRow): Registration { + return { + id: row.id as ClientId, + serverId: (row.serverId ?? undefined) as Optional, + clientId: row.clientId as ClientId, + machineId: row.machineId, + // because these are just strings of digits, sqlite may return them as + // numbers, so we have to convert them back to strings + commonAccessCardId: row.commonAccessCardId.toString(), + registrationRequestId: row.registrationRequestId as ClientId, + electionId: row.electionId as ClientId, + precinctId: row.precinctId, + ballotStyleId: row.ballotStyleId, + createdAt: DateTime.fromSQL(row.createdAt), + }; +} + +export function deserializeServerSyncAttempt( + row: ServerSyncAttemptRow +): ServerSyncAttempt { + return { + id: row.id as ClientId, + creator: row.creator, + trigger: row.trigger, + statusMessage: row.statusMessage, + createdAt: DateTime.fromSQL(row.createdAt), + success: row.success === 1 ? true : row.success === 0 ? false : undefined, + completedAt: row.completedAt + ? DateTime.fromSQL(row.completedAt) + : undefined, + }; +} diff --git a/apps/rave-mark/backend/src/types/sync.ts b/apps/rave-mark/backend/src/types/sync.ts new file mode 100644 index 0000000000..5641f5a05d --- /dev/null +++ b/apps/rave-mark/backend/src/types/sync.ts @@ -0,0 +1,230 @@ +import { Buffer } from 'buffer'; +import { z } from 'zod'; +import { + BallotStyleId, + BallotStyleIdSchema, + Id, + NewType, + PrecinctId, + PrecinctIdSchema, +} from '@votingworks/types'; +import { DateTime } from 'luxon'; +import { ClientId, ClientIdSchema, ServerId, ServerIdSchema } from './db'; + +export type Base64String = NewType; +export const Base64StringSchema = z.string().superRefine((value, ctx) => { + try { + Buffer.from(value, 'base64'); + return true; + } catch (error) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ctx.path, + message: `invalid base64 string: ${(error as Error).message}`, + }); + } +}) as unknown as z.ZodSchema; + +export const Base64Buffer = z + .string() + .transform((value) => + Buffer.from(value, 'base64') + ) as unknown as z.ZodSchema; + +export const DateTimeSchema = z + .string() + .transform((value) => + DateTime.fromISO(value) + ) as unknown as z.ZodSchema; + +export interface RegistrationRequestInput { + clientId: ClientId; + machineId: Id; + commonAccessCardId: string; + givenName: string; + familyName: string; + addressLine1: string; + addressLine2?: string; + city: string; + state: string; + postalCode: string; + stateId: string; +} + +export type RegistrationRequestOutput = RegistrationRequestInput & { + serverId: ServerId; + createdAt: DateTime; +}; + +export const RegistrationRequestOutputSchema: z.ZodSchema = + z.object({ + serverId: ServerIdSchema, + clientId: ClientIdSchema, + machineId: z.string(), + commonAccessCardId: z.string(), + givenName: z.string(), + familyName: z.string(), + addressLine1: z.string(), + addressLine2: z.string().optional(), + city: z.string(), + state: z.string(), + postalCode: z.string(), + stateId: z.string(), + createdAt: DateTimeSchema, + }); + +export interface RegistrationInput { + clientId: ClientId; + machineId: Id; + commonAccessCardId: Id; + registrationRequestId: ClientId; + electionId: ClientId; + precinctId: PrecinctId; + ballotStyleId: BallotStyleId; +} + +export interface RegistrationOutput { + serverId: ServerId; + clientId: ClientId; + machineId: Id; + commonAccessCardId: Id; + registrationRequestId: ServerId; + electionId: ServerId; + precinctId: PrecinctId; + ballotStyleId: BallotStyleId; + createdAt: DateTime; +} + +export const RegistrationOutputSchema: z.ZodSchema = + z.object({ + serverId: ServerIdSchema, + clientId: ClientIdSchema, + machineId: z.string(), + commonAccessCardId: z.string(), + registrationRequestId: ServerIdSchema, + electionId: ServerIdSchema, + precinctId: PrecinctIdSchema, + ballotStyleId: BallotStyleIdSchema, + createdAt: DateTimeSchema, + }); + +export interface PrintedBallotInput { + clientId: ClientId; + machineId: Id; + commonAccessCardId: string; + commonAccessCardCertificate: Base64String; + registrationId: ClientId; + castVoteRecord: Base64String; +} + +export interface ScannedBallotInput { + clientId: ClientId; + machineId: Id; + electionId: ClientId; + castVoteRecord: Base64String; +} + +export interface PrintedBallotOutput { + serverId: ServerId; + clientId: ClientId; + machineId: Id; + commonAccessCardId: string; + commonAccessCardCertificate: Base64String; + registrationId: ServerId; + castVoteRecord: Base64String; + castVoteRecordSignature: Base64String; +} + +export const PrintedBallotOutputSchema: z.ZodSchema = + z.object({ + serverId: ServerIdSchema, + clientId: ClientIdSchema, + machineId: z.string(), + commonAccessCardId: z.string(), + commonAccessCardCertificate: Base64StringSchema, + registrationId: ServerIdSchema, + castVoteRecord: Base64StringSchema, + castVoteRecordSignature: Base64StringSchema, + }); + +export interface ScannedBallotOutput { + serverId: ServerId; + clientId: ClientId; + machineId: Id; + electionId: ServerId; + castVoteRecord: Buffer; +} + +export const ScannedBallotOutputSchema: z.ZodSchema = + z.object({ + serverId: ServerIdSchema, + clientId: ClientIdSchema, + machineId: z.string(), + electionId: ServerIdSchema, + castVoteRecord: Base64Buffer, + }); + +export interface AdminInput { + machineId: string; + commonAccessCardId: string; + createdAt: DateTime; +} + +export type AdminOutput = AdminInput; + +export const AdminOutputSchema: z.ZodSchema = z.object({ + machineId: z.string(), + commonAccessCardId: z.string(), + createdAt: DateTimeSchema, +}); + +export interface ElectionInput { + clientId: ClientId; + machineId: Id; + definition: Base64String; +} + +export type ElectionOutput = ElectionInput & { + serverId: ServerId; + createdAt: DateTime; +}; + +export const ElectionOutputSchema: z.ZodSchema = z.object({ + serverId: ServerIdSchema, + clientId: ClientIdSchema, + machineId: z.string(), + definition: Base64StringSchema, + createdAt: DateTimeSchema, +}); + +export interface RaveServerSyncInput { + lastSyncedRegistrationRequestId?: ServerId; + lastSyncedRegistrationId?: ServerId; + lastSyncedElectionId?: ServerId; + lastSyncedPrintedBallotId?: ServerId; + lastSyncedScannedBallotId?: ServerId; + registrationRequests?: RegistrationRequestInput[]; + elections?: ElectionInput[]; + registrations?: RegistrationInput[]; + printedBallots?: PrintedBallotInput[]; + scannedBallots?: ScannedBallotInput[]; +} + +export interface RaveServerSyncOutput { + admins: AdminOutput[]; + elections: ElectionOutput[]; + registrationRequests: RegistrationRequestOutput[]; + registrations: RegistrationOutput[]; + printedBallots: PrintedBallotOutput[]; + scannedBallots: ScannedBallotOutput[]; +} + +export const RaveMarkSyncOutputSchema: z.ZodSchema = + z.object({ + admins: z.array(AdminOutputSchema), + elections: z.array(ElectionOutputSchema), + registrationRequests: z.array(RegistrationRequestOutputSchema), + registrations: z.array(RegistrationOutputSchema), + printedBallots: z.array(PrintedBallotOutputSchema), + scannedBallots: z.array(ScannedBallotOutputSchema), + }); diff --git a/apps/rave-mark/backend/src/workspace.test.ts b/apps/rave-mark/backend/src/workspace.test.ts new file mode 100644 index 0000000000..0b4d43a375 --- /dev/null +++ b/apps/rave-mark/backend/src/workspace.test.ts @@ -0,0 +1,10 @@ +import { dirSync } from 'tmp'; +import { createWorkspace } from './workspace'; + +test('workspace.reset rests the store', () => { + const workspace = createWorkspace(dirSync().name); + const fn = jest.fn(); + workspace.store.reset = fn; + workspace.reset(); + expect(fn).toHaveBeenCalledTimes(1); +}); diff --git a/apps/rave-mark/backend/src/workspace.ts b/apps/rave-mark/backend/src/workspace.ts new file mode 100644 index 0000000000..543656bc26 --- /dev/null +++ b/apps/rave-mark/backend/src/workspace.ts @@ -0,0 +1,54 @@ +import { ensureDirSync } from 'fs-extra'; +import { join, resolve } from 'path'; +import { LogEventId, Logger } from '@votingworks/logging'; +import { Store } from './store'; +import { RAVE_MARK_WORKSPACE } from './globals'; + +export interface Workspace { + /** + * The path to the workspace root. + */ + readonly path: string; + + /** + * The store associated with the workspace. + */ + readonly store: Store; + + /** + * Reset the workspace, including the election configuration. This is the same + * as deleting the workspace and recreating it. + */ + reset(): void; +} + +export function createWorkspace(root: string): Workspace { + const resolvedRoot = resolve(root); + ensureDirSync(resolvedRoot); + + const dbPath = join(resolvedRoot, 'rave-mark.db'); + const store = Store.fileStore(dbPath); + + return { + path: resolvedRoot, + store, + reset() { + store.reset(); + }, + }; +} + +export async function resolveWorkspace(logger?: Logger): Promise { + const workspacePath = RAVE_MARK_WORKSPACE; + if (!workspacePath) { + await logger?.log(LogEventId.ScanServiceConfigurationMessage, 'system', { + message: + 'workspace path could not be determined; pass a workspace or run with MARK_WORKSPACE', + disposition: 'failure', + }); + throw new Error( + 'workspace path could not be determined; pass a workspace or run with MARK_WORKSPACE' + ); + } + return createWorkspace(workspacePath); +} diff --git a/apps/rave-mark/backend/test/app_helpers.ts b/apps/rave-mark/backend/test/app_helpers.ts index 560e6f92b4..19c542c29d 100644 --- a/apps/rave-mark/backend/test/app_helpers.ts +++ b/apps/rave-mark/backend/test/app_helpers.ts @@ -1,31 +1,77 @@ -import { - ArtifactAuthenticatorApi, - buildMockArtifactAuthenticator, - buildMockInsertedSmartCardAuth, - InsertedSmartCardAuthApi, -} from '@votingworks/auth'; -import { createMockUsb, MockUsb } from '@votingworks/backend'; +import { Buffer } from 'buffer'; import * as grout from '@votingworks/grout'; import { Application } from 'express'; import { Server } from 'http'; import { AddressInfo } from 'net'; +import { dirSync } from 'tmp'; import { Api, buildApp } from '../src/app'; +import { createWorkspace } from '../src/workspace'; +import { MockRaveServerClient } from '../src/rave_server_client'; +import { Store } from '../src/store'; +import { Auth, AuthStatus } from '../src/types/auth'; interface MockAppContents { apiClient: grout.Client; app: Application; - mockAuth: InsertedSmartCardAuthApi; - mockArtifactAuthenticator: ArtifactAuthenticatorApi; - mockUsb: MockUsb; + mockAuth: Auth; server: Server; } +export function buildMockAuth({ + pin: actualPin = '000000', +}: { pin?: string } = {}): Auth { + let currentStatus: AuthStatus = { + status: 'no_card', + }; + + return { + getAuthStatus() { + return Promise.resolve(currentStatus); + }, + + checkPin(pin) { + if (pin === actualPin) { + currentStatus = { + status: 'has_card', + card: { + givenName: 'Joe', + familyName: 'Smith', + commonAccessCardId: '1234567890', + }, + isAdmin: false, + }; + return Promise.resolve(true); + } + + return Promise.resolve(false); + }, + + generateSignature() { + return Promise.resolve(Buffer.from('signature')); + }, + + getCertificate() { + return Promise.resolve(Buffer.from('certificate')); + }, + + logOut() { + currentStatus = { + status: 'no_card', + }; + return Promise.resolve(); + }, + }; +} + export function createApp(): MockAppContents { - const mockAuth = buildMockInsertedSmartCardAuth(); - const mockArtifactAuthenticator = buildMockArtifactAuthenticator(); - const mockUsb = createMockUsb(); + const mockAuth = buildMockAuth(); + const workdir = dirSync().name; - const app = buildApp(); + const app = buildApp({ + auth: mockAuth, + workspace: createWorkspace(workdir), + raveServerClient: new MockRaveServerClient(Store.memoryStore()), + }); const server = app.listen(); const { port } = server.address() as AddressInfo; @@ -37,8 +83,6 @@ export function createApp(): MockAppContents { apiClient, app, mockAuth, - mockArtifactAuthenticator, - mockUsb, server, }; } diff --git a/apps/rave-mark/backend/tsconfig.build.json b/apps/rave-mark/backend/tsconfig.build.json index a4bd1674c8..0c83f967cf 100644 --- a/apps/rave-mark/backend/tsconfig.build.json +++ b/apps/rave-mark/backend/tsconfig.build.json @@ -14,7 +14,6 @@ { "path": "../../../libs/backend/tsconfig.build.json" }, { "path": "../../../libs/basics/tsconfig.build.json" }, { "path": "../../../libs/db/tsconfig.build.json" }, - { "path": "../../../libs/dev-dock/backend/tsconfig.build.json" }, { "path": "../../../libs/eslint-plugin-vx/tsconfig.build.json" }, { "path": "../../../libs/fixtures/tsconfig.build.json" }, { "path": "../../../libs/grout/tsconfig.build.json" }, diff --git a/apps/rave-mark/backend/tsconfig.json b/apps/rave-mark/backend/tsconfig.json index bfe2025256..06e34b667a 100644 --- a/apps/rave-mark/backend/tsconfig.json +++ b/apps/rave-mark/backend/tsconfig.json @@ -20,7 +20,6 @@ { "path": "../../../libs/backend/tsconfig.build.json" }, { "path": "../../../libs/basics/tsconfig.build.json" }, { "path": "../../../libs/db/tsconfig.build.json" }, - { "path": "../../../libs/dev-dock/backend/tsconfig.build.json" }, { "path": "../../../libs/eslint-plugin-vx/tsconfig.build.json" }, { "path": "../../../libs/fixtures/tsconfig.build.json" }, { "path": "../../../libs/grout/tsconfig.build.json" }, diff --git a/apps/rave-mark/frontend/.eslintrc.json b/apps/rave-mark/frontend/.eslintrc.json index 2d07e49e00..3c2df7695b 100644 --- a/apps/rave-mark/frontend/.eslintrc.json +++ b/apps/rave-mark/frontend/.eslintrc.json @@ -1,6 +1,7 @@ { "extends": ["plugin:vx/react"], "rules": { - "vx/gts-jsdoc": "off" + "vx/gts-jsdoc": "off", + "vx/gts-direct-module-export-access-only": "off" } } diff --git a/apps/rave-mark/frontend/.stylelintrc.js b/apps/rave-mark/frontend/.stylelintrc.js deleted file mode 100644 index 7d134e80c3..0000000000 --- a/apps/rave-mark/frontend/.stylelintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -// Ensure we use all available babel parser plugins, but not the type annotation -// ones since stylelint handles that itself. -const { buildOptions } = require('@codemod/parser') -const parserPlugins = buildOptions().plugins.filter(plugin => plugin !== 'typescript' && plugin !== 'flow') - -// Config for stylelint parsing CSS in Styled Components -// Note the `--fix` flag doesn't yet work for CSS-in-JS -module.exports = { - processors: [['stylelint-processor-styled-components', { parserPlugins }]], - extends: [ - 'stylelint-config-palantir', - 'stylelint-config-prettier', - 'stylelint-config-styled-components', - ], - rules: { - 'selector-max-id': 1, - 'selector-max-universal': 1, - }, -} diff --git a/apps/rave-mark/frontend/Makefile b/apps/rave-mark/frontend/Makefile new file mode 100644 index 0000000000..5658de32af --- /dev/null +++ b/apps/rave-mark/frontend/Makefile @@ -0,0 +1,26 @@ +BACKEND := ../backend + +# a phony dependency that can be used as a dependency to force builds +FORCE: + +# install any required dependencies here, i.e. `sudo apt install -y DEPS` +install: + +build-backend: + make -C $(BACKEND) install; \ + make -C $(BACKEND) build; \ + +build-frontend: build + +build-all: build-backend build-frontend + +run-all: + (trap 'kill 0' SIGINT SIGHUP; make -C $(BACKEND) run & cd prodserver && node index.js) + +build: FORCE + pnpm install && pnpm build + +bootstrap: install build + +run: + cd prodserver && node index.js diff --git a/apps/rave-mark/frontend/jest.config.js b/apps/rave-mark/frontend/jest.config.js index 9e136d903a..b9fbf7ed11 100644 --- a/apps/rave-mark/frontend/jest.config.js +++ b/apps/rave-mark/frontend/jest.config.js @@ -13,6 +13,7 @@ module.exports = { transformIgnorePatterns: [ '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$', ], + setupFilesAfterEnv: ['/test/setup.ts'], coverageThreshold: { global: { statements: 0, diff --git a/apps/rave-mark/frontend/package.json b/apps/rave-mark/frontend/package.json index 5f5f1bab68..ff11610046 100644 --- a/apps/rave-mark/frontend/package.json +++ b/apps/rave-mark/frontend/package.json @@ -11,24 +11,23 @@ ], "scripts": { "build": "pnpm type-check && vite build", + "clean": "rm -rf build tsconfig.tsbuildinfo tsconfig.build.tsbuildinfo", "format": "prettier '**/*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)' --write", - "lint": "pnpm type-check && eslint . && pnpm stylelint:run", - "lint:fix": "pnpm type-check && eslint . --fix && pnpm stylelint:run --fix", + "lint": "pnpm type-check && eslint .", + "lint:fix": "pnpm type-check && eslint . --fix", "pre-commit": "lint-staged", - "start": "pnpm -w run-dev rave-mark", + "start": "TZ=UTC pnpm -w run-dev rave-mark", "start:core": "pnpm -w run-dev vm-rave-mark --core-only", "start:prod": "concurrently --names frontend,backend 'pnpm --dir prodserver start' 'pnpm --dir ../backend start'", - "stylelint:run": "stylelint 'src/**/*.{js,jsx,ts,tsx}'", "test": "is-ci test:ci test:watch", "test:ci": "TZ=UTC CI=true jest --maxWorkers=7", "test:coverage": "TZ=UTC jest --coverage --watchAll=false", "test:update": "TZ=UTC jest -u --watchAll=false", - "test:watch": "TZ=UTC jest", + "test:watch": "TZ=UTC jest --watch", "type-check": "tsc --build" }, "lint-staged": { "*.+(js|jsx|ts|tsx)": [ - "stylelint", "eslint --quiet --fix" ], "*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)": [ @@ -39,9 +38,8 @@ ] }, "dependencies": { - "@tanstack/react-query": "^4.22.0", + "@tanstack/react-query": "4.32.1", "@votingworks/basics": "workspace:*", - "@votingworks/dev-dock-frontend": "workspace:^", "@votingworks/grout": "workspace:*", "@votingworks/logging": "workspace:*", "@votingworks/mark-flow-ui": "workspace:*", @@ -50,45 +48,49 @@ "@votingworks/ui": "workspace:*", "@votingworks/utils": "workspace:*", "buffer": "^6.0.3", + "luxon": "^3.0.0", "path": "^0.12.7", - "react": "17.0.1", - "react-dom": "17.0.1", - "react-router-dom": "^5.2.0" + "react": "18.2.0", + "react-dom": "18.2.0", + "styled-components": "^5.3.11" }, "devDependencies": { - "@testing-library/react": "^12.1.5", - "@types/jest": "^29.5.2", - "@types/react": "17.0.39", - "@types/react-dom": "^18.2.4", - "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "5.37.0", - "@typescript-eslint/parser": "5.37.0", + "@jest/types": "^29.6.1", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^29.5.3", + "@types/luxon": "^3.0.0", + "@types/react": "18.2.18", + "@types/react-dom": "^18.2.7", + "@types/styled-components": "^5.1.26", + "@types/testing-library__jest-dom": "^5.14.9", "@vitejs/plugin-react": "^1.3.2", + "@votingworks/grout-test-utils": "workspace:*", "@votingworks/monorepo-utils": "workspace:*", - "concurrently": "^7.4.0", - "eslint": "8.23.1", + "@votingworks/test-utils": "workspace:*", + "concurrently": "7.6.0", + "eslint": "8.51.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^9.0.0", "eslint-config-react-app": "^7.0.1", - "eslint-import-resolver-node": "^0.3.4", - "eslint-plugin-flowtype": "^5.2.0", + "eslint-import-resolver-node": "^0.3.9", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^26.1.5", + "eslint-plugin-jest": "^27.2.3", "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-testing-library": "^5.6.4", "eslint-plugin-vx": "workspace:*", - "is-ci-cli": "^2.1.2", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.4.1", - "jest-junit": "^14.0.1", - "jest-watch-typeahead": "^0.6.4", - "lint-staged": "^10.5.3", - "ts-jest": "^29.1.0", - "typescript": "4.6.3", - "vite": "^4.3.9" + "is-ci-cli": "2.2.0", + "jest": "^29.6.2", + "jest-environment-jsdom": "^29.6.2", + "jest-junit": "^16.0.0", + "jest-watch-typeahead": "^2.2.2", + "lint-staged": "11.0.0", + "ts-jest": "29.1.1", + "vite": "4.5.0" }, "packageManager": "pnpm@8.1.0", "vx": { diff --git a/apps/rave-mark/frontend/prodserver/package.json b/apps/rave-mark/frontend/prodserver/package.json index a4019cbd09..1f07635e8a 100644 --- a/apps/rave-mark/frontend/prodserver/package.json +++ b/apps/rave-mark/frontend/prodserver/package.json @@ -6,7 +6,7 @@ "start": "node index.js" }, "dependencies": { - "express": "^4.17.1", + "express": "4.18.2", "http-proxy-middleware": "1.0.6" } } diff --git a/apps/rave-mark/frontend/prodserver/setupProxy.js b/apps/rave-mark/frontend/prodserver/setupProxy.js index ad3d748fec..9c078ae33e 100644 --- a/apps/rave-mark/frontend/prodserver/setupProxy.js +++ b/apps/rave-mark/frontend/prodserver/setupProxy.js @@ -10,9 +10,9 @@ const { createProxyMiddleware: proxy } = require('http-proxy-middleware'); /** * @param {import('connect').Server} app + * @param {number=} basePort */ -module.exports = function (app) { - app.use(proxy('/api', { target: 'http://localhost:3002/' })); - app.use(proxy('/dock', { target: 'http://localhost:3002/' })); - app.use(proxy('/card', { target: 'http://localhost:3001/' })); +module.exports = function (app, basePort = 3000) { + app.use(proxy('/api', { target: `http://localhost:${basePort + 2}/` })); + app.use(proxy('/dock', { target: `http://localhost:${basePort + 2}/` })); }; diff --git a/apps/rave-mark/frontend/public/seals/Alabama-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Alabama-StateSeal.svg new file mode 100644 index 0000000000..ee23c3604b --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Alabama-StateSeal.svg @@ -0,0 +1,1395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Alaska-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Alaska-StateSeal.svg new file mode 100644 index 0000000000..9d5bfc88b0 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Alaska-StateSeal.svg @@ -0,0 +1,4317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Arizona-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Arizona-StateSeal.svg new file mode 100644 index 0000000000..24eb8a73cd --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Arizona-StateSeal.svg @@ -0,0 +1,1599 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Arkansas-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Arkansas-StateSeal.svg new file mode 100644 index 0000000000..8598797644 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Arkansas-StateSeal.svg @@ -0,0 +1,1406 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Colorado-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Colorado-StateSeal.svg new file mode 100644 index 0000000000..9df53bc8a9 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Colorado-StateSeal.svg @@ -0,0 +1,2473 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Connecticut-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Connecticut-StateSeal.svg new file mode 100644 index 0000000000..16fc2aa600 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Connecticut-StateSeal.svg @@ -0,0 +1,1607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Delaware-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Delaware-StateSeal.svg new file mode 100644 index 0000000000..ff2f9cbca6 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Delaware-StateSeal.svg @@ -0,0 +1,1370 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Florida-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Florida-StateSeal.svg new file mode 100644 index 0000000000..46e240ca26 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Florida-StateSeal.svg @@ -0,0 +1,3236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Georgia-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Georgia-StateSeal.svg new file mode 100644 index 0000000000..c34cac0b54 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Georgia-StateSeal.svg @@ -0,0 +1,3910 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Great_Seal_of_Montana.svg b/apps/rave-mark/frontend/public/seals/Great_Seal_of_Montana.svg new file mode 100644 index 0000000000..9e706facce --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Great_Seal_of_Montana.svg @@ -0,0 +1,4629 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Great_Seal_of_the_State_of_Louisiana.svg b/apps/rave-mark/frontend/public/seals/Great_Seal_of_the_State_of_Louisiana.svg new file mode 100644 index 0000000000..504682fba8 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Great_Seal_of_the_State_of_Louisiana.svg @@ -0,0 +1,1660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Hawaii-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Hawaii-StateSeal.svg new file mode 100644 index 0000000000..dbf8e23cf2 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Hawaii-StateSeal.svg @@ -0,0 +1,7328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Idaho-StateSeal-NARA.svg b/apps/rave-mark/frontend/public/seals/Idaho-StateSeal-NARA.svg new file mode 100644 index 0000000000..35b255c7d3 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Idaho-StateSeal-NARA.svg @@ -0,0 +1,2211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Idaho-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Idaho-StateSeal.svg new file mode 100644 index 0000000000..cec3051b6f --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Idaho-StateSeal.svg @@ -0,0 +1,4862 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Illinois-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Illinois-StateSeal.svg new file mode 100644 index 0000000000..14348eb905 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Illinois-StateSeal.svg @@ -0,0 +1,3602 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Indiana-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Indiana-StateSeal.svg new file mode 100644 index 0000000000..f039822561 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Indiana-StateSeal.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/rave-mark/frontend/public/seals/Iowa-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Iowa-StateSeal.svg new file mode 100644 index 0000000000..05e6f5fcfb --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Iowa-StateSeal.svg @@ -0,0 +1,6576 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Kansas-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Kansas-StateSeal.svg new file mode 100644 index 0000000000..0c3a9bf609 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Kansas-StateSeal.svg @@ -0,0 +1,7184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Kentucky-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Kentucky-StateSeal.svg new file mode 100644 index 0000000000..f80ac6a371 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Kentucky-StateSeal.svg @@ -0,0 +1,1707 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Louisiana-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Louisiana-StateSeal.svg new file mode 100644 index 0000000000..87c1d33b7d --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Louisiana-StateSeal.svg @@ -0,0 +1,3931 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Maine-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Maine-StateSeal.svg new file mode 100644 index 0000000000..0ba224320b --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Maine-StateSeal.svg @@ -0,0 +1,3810 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Massachusetts-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Massachusetts-StateSeal.svg new file mode 100644 index 0000000000..c1e388729e --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Massachusetts-StateSeal.svg @@ -0,0 +1,2627 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Michigan-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Michigan-StateSeal.svg new file mode 100644 index 0000000000..295b372c8a --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Michigan-StateSeal.svg @@ -0,0 +1,2247 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Minnesota-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Minnesota-StateSeal.svg new file mode 100644 index 0000000000..6e6eda76af --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Minnesota-StateSeal.svg @@ -0,0 +1,2722 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Missouri-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Missouri-StateSeal.svg new file mode 100644 index 0000000000..107a7e2431 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Missouri-StateSeal.svg @@ -0,0 +1,3207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Montana-StateSeal-NARA.svg b/apps/rave-mark/frontend/public/seals/Montana-StateSeal-NARA.svg new file mode 100644 index 0000000000..6d5d9fac64 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Montana-StateSeal-NARA.svg @@ -0,0 +1,976 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Nebraska-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Nebraska-StateSeal.svg new file mode 100644 index 0000000000..3b80929b85 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Nebraska-StateSeal.svg @@ -0,0 +1,4967 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Nevada-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Nevada-StateSeal.svg new file mode 100644 index 0000000000..a21af33008 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Nevada-StateSeal.svg @@ -0,0 +1,5256 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/NewHampshire-StateSeal-NARA.svg b/apps/rave-mark/frontend/public/seals/NewHampshire-StateSeal-NARA.svg new file mode 100644 index 0000000000..709211d9da --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/NewHampshire-StateSeal-NARA.svg @@ -0,0 +1,1077 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/NewYork-StateSeal.svg b/apps/rave-mark/frontend/public/seals/NewYork-StateSeal.svg new file mode 100644 index 0000000000..0c6f4ab9cb --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/NewYork-StateSeal.svg @@ -0,0 +1,6436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/New_Hampshire_Moultonborough.svg b/apps/rave-mark/frontend/public/seals/New_Hampshire_Moultonborough.svg new file mode 100644 index 0000000000..fb04cff892 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/New_Hampshire_Moultonborough.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/NorthCarolina-StateSeal.svg b/apps/rave-mark/frontend/public/seals/NorthCarolina-StateSeal.svg new file mode 100644 index 0000000000..e034c75a12 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/NorthCarolina-StateSeal.svg @@ -0,0 +1,4948 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/NorthDakota-StateSeal.svg b/apps/rave-mark/frontend/public/seals/NorthDakota-StateSeal.svg new file mode 100644 index 0000000000..2d8005a974 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/NorthDakota-StateSeal.svg @@ -0,0 +1,3253 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Oregon-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Oregon-StateSeal.svg new file mode 100644 index 0000000000..6a85332a5a --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Oregon-StateSeal.svg @@ -0,0 +1,2447 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Pennsylvania-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Pennsylvania-StateSeal.svg new file mode 100644 index 0000000000..0ddcf604bc --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Pennsylvania-StateSeal.svg @@ -0,0 +1,1425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/RhodeIsland-StateSeal.svg b/apps/rave-mark/frontend/public/seals/RhodeIsland-StateSeal.svg new file mode 100644 index 0000000000..cb62b1e95e --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/RhodeIsland-StateSeal.svg @@ -0,0 +1,4631 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Sample-Seal.svg b/apps/rave-mark/frontend/public/seals/Sample-Seal.svg new file mode 100644 index 0000000000..b3a9131ba7 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Sample-Seal.svg @@ -0,0 +1,4 @@ + + + +Seal of Montgomery County, Maryland. diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Alabama.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Alabama.svg new file mode 100644 index 0000000000..d63ae28e07 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Alabama.svg @@ -0,0 +1,7 @@ + + +image/svg+xmlGreat Seal of The State of Alabama + +Great Seal of The State of Alabama + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Arkansas.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Arkansas.svg new file mode 100644 index 0000000000..994bc92980 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Arkansas.svg @@ -0,0 +1,1802 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_California.svg b/apps/rave-mark/frontend/public/seals/Seal_of_California.svg new file mode 100644 index 0000000000..28a8d7ab7f --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_California.svg @@ -0,0 +1,4644 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +ring + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + outer text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +X//lT/8vLbz0eOHv/u6f/vf/9W//5v8D8/klow== + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Colorado.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Colorado.svg new file mode 100644 index 0000000000..e9db04c5da --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Colorado.svg @@ -0,0 +1,880 @@ + + + +image/svg+xml + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Connecticut.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Connecticut.svg new file mode 100644 index 0000000000..4c00e07e36 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Connecticut.svg @@ -0,0 +1,439 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Delaware.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Delaware.svg new file mode 100644 index 0000000000..e941bc8c20 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Delaware.svg @@ -0,0 +1,1075 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Florida.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Florida.svg new file mode 100644 index 0000000000..3af4197e0f --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Florida.svg @@ -0,0 +1,6846 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Georgia.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Georgia.svg new file mode 100644 index 0000000000..4c8e0c4cc1 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Georgia.svg @@ -0,0 +1,673 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Idaho.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Idaho.svg new file mode 100644 index 0000000000..59b8008a5b --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Idaho.svg @@ -0,0 +1,3461 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Illinois.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Illinois.svg new file mode 100644 index 0000000000..7d1fe1cc28 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Illinois.svg @@ -0,0 +1,3362 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Kansas.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Kansas.svg new file mode 100644 index 0000000000..ae0d75ed7b --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Kansas.svg @@ -0,0 +1,3765 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Kentucky.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Kentucky.svg new file mode 100644 index 0000000000..a27f91ad36 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Kentucky.svg @@ -0,0 +1,3058 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Louisiana.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Louisiana.svg new file mode 100644 index 0000000000..c3e0f54eb3 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Louisiana.svg @@ -0,0 +1,2015 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Maine.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Maine.svg new file mode 100644 index 0000000000..cddf9e00df --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Maine.svg @@ -0,0 +1,1995 @@ + + + +image/svg+xml + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Maryland_%28reverse%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Maryland_%28reverse%29.svg new file mode 100644 index 0000000000..de7c741916 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Maryland_%28reverse%29.svg @@ -0,0 +1,5353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Massachusetts.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Massachusetts.svg new file mode 100644 index 0000000000..bf12012f37 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Massachusetts.svg @@ -0,0 +1,3134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Michigan.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Michigan.svg new file mode 100644 index 0000000000..14cdccffba --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Michigan.svg @@ -0,0 +1,827 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Minnesota_%281858%E2%80%931971%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Minnesota_%281858%E2%80%931971%29.svg new file mode 100644 index 0000000000..9f750492a2 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Minnesota_%281858%E2%80%931971%29.svg @@ -0,0 +1,1245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_1879-2014.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_1879-2014.svg new file mode 100644 index 0000000000..386058a249 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_1879-2014.svg @@ -0,0 +1,1788 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_2014-present.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_2014-present.svg new file mode 100644 index 0000000000..1af7e4e993 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_2014-present.svg @@ -0,0 +1,1727 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_BW.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_BW.svg new file mode 100644 index 0000000000..bca04e1c97 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Mississippi_BW.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Missouri.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Missouri.svg new file mode 100644 index 0000000000..c2e6a67f8e --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Missouri.svg @@ -0,0 +1,6626 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Nebraska.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Nebraska.svg new file mode 100644 index 0000000000..bd480d544d --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Nebraska.svg @@ -0,0 +1,2388 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_New_Hampshire.svg b/apps/rave-mark/frontend/public/seals/Seal_of_New_Hampshire.svg new file mode 100644 index 0000000000..11c6bf1266 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_New_Hampshire.svg @@ -0,0 +1,1387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_New_Jersey.svg b/apps/rave-mark/frontend/public/seals/Seal_of_New_Jersey.svg new file mode 100644 index 0000000000..9b53fc17e7 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_New_Jersey.svg @@ -0,0 +1,2193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_New_Mexico.svg b/apps/rave-mark/frontend/public/seals/Seal_of_New_Mexico.svg new file mode 100644 index 0000000000..31affce6eb --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_New_Mexico.svg @@ -0,0 +1,3910 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_New_York.svg b/apps/rave-mark/frontend/public/seals/Seal_of_New_York.svg new file mode 100644 index 0000000000..37c0e30257 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_New_York.svg @@ -0,0 +1,1610 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina.svg b/apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina.svg new file mode 100644 index 0000000000..98f9fea36a --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina.svg @@ -0,0 +1,4656 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina_%281971-1984%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina_%281971-1984%29.svg new file mode 100644 index 0000000000..e279c5b05d --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_North_Carolina_%281971-1984%29.svg @@ -0,0 +1,4649 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Ohio.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Ohio.svg new file mode 100644 index 0000000000..944aea2104 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Ohio.svg @@ -0,0 +1,811 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Ohio_%28Official%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Ohio_%28Official%29.svg new file mode 100644 index 0000000000..0fc98424fd --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Ohio_%28Official%29.svg @@ -0,0 +1,903 @@ + + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma.svg new file mode 100644 index 0000000000..c2058fd433 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma.svg @@ -0,0 +1,4021 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma_%28B%26W%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma_%28B%26W%29.svg new file mode 100644 index 0000000000..4340c4a0a2 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Oklahoma_%28B%26W%29.svg @@ -0,0 +1,3471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Oregon.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Oregon.svg new file mode 100644 index 0000000000..147b1b5761 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Oregon.svg @@ -0,0 +1,2896 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Pennsylvania.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Pennsylvania.svg new file mode 100644 index 0000000000..ccc4fe615a --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Pennsylvania.svg @@ -0,0 +1,1063 @@ + + + +image/svg+xml + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Rhode_Island.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Rhode_Island.svg new file mode 100644 index 0000000000..91dbc0cb39 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Rhode_Island.svg @@ -0,0 +1,3986 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina.svg b/apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina.svg new file mode 100644 index 0000000000..6e08f75f08 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina.svg @@ -0,0 +1,1918 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina_%28Alternative%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina_%28Alternative%29.svg new file mode 100644 index 0000000000..bcb459dd37 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_South_Carolina_%28Alternative%29.svg @@ -0,0 +1,2008 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_South_Dakota_%28B%26W%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_South_Dakota_%28B%26W%29.svg new file mode 100644 index 0000000000..78e1b53c2a --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_South_Dakota_%28B%26W%29.svg @@ -0,0 +1,3120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Tennessee.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Tennessee.svg new file mode 100644 index 0000000000..f6051eca4b --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Tennessee.svg @@ -0,0 +1,3113 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Texas.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Texas.svg new file mode 100644 index 0000000000..6f239dc29c --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Texas.svg @@ -0,0 +1,1431 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Utah.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Utah.svg new file mode 100644 index 0000000000..f732401b71 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Utah.svg @@ -0,0 +1,2814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28Alternate%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28Alternate%29.svg new file mode 100644 index 0000000000..1e5ee2af79 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28Alternate%29.svg @@ -0,0 +1,2542 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%29_2011.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%29_2011.svg new file mode 100644 index 0000000000..e717e91579 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%29_2011.svg @@ -0,0 +1,2777 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%2C_enhanced_variant%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%2C_enhanced_variant%29.svg new file mode 100644 index 0000000000..7bc5eb58c5 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28alternative%2C_enhanced_variant%29.svg @@ -0,0 +1,19936 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28enhanced_variant%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28enhanced_variant%29.svg new file mode 100644 index 0000000000..72fc807f53 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Utah_%28enhanced_variant%29.svg @@ -0,0 +1,17069 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Virginia.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Virginia.svg new file mode 100644 index 0000000000..8065142409 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Virginia.svg @@ -0,0 +1,3348 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Washington.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Washington.svg new file mode 100644 index 0000000000..d33b31c34d --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Washington.svg @@ -0,0 +1,2016 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_West_Virginia.svg b/apps/rave-mark/frontend/public/seals/Seal_of_West_Virginia.svg new file mode 100644 index 0000000000..798fec07f1 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_West_Virginia.svg @@ -0,0 +1,2536 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Wisconsin.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Wisconsin.svg new file mode 100644 index 0000000000..a3962818eb --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Wisconsin.svg @@ -0,0 +1,2794 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_Wyoming.svg b/apps/rave-mark/frontend/public/seals/Seal_of_Wyoming.svg new file mode 100644 index 0000000000..5a225b833e --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_Wyoming.svg @@ -0,0 +1,821 @@ + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_the_Connecticut_Department_of_Transportation.svg b/apps/rave-mark/frontend/public/seals/Seal_of_the_Connecticut_Department_of_Transportation.svg new file mode 100644 index 0000000000..4aeaef9370 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_the_Connecticut_Department_of_Transportation.svg @@ -0,0 +1,265 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_the_House_of_Representatives_of_Massachusetts.svg b/apps/rave-mark/frontend/public/seals/Seal_of_the_House_of_Representatives_of_Massachusetts.svg new file mode 100644 index 0000000000..e7232b53a2 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_the_House_of_Representatives_of_Massachusetts.svg @@ -0,0 +1,802 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_the_Senate_of_Massachusetts_%28variant%29.svg b/apps/rave-mark/frontend/public/seals/Seal_of_the_Senate_of_Massachusetts_%28variant%29.svg new file mode 100644 index 0000000000..3d6a9192b4 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_the_Senate_of_Massachusetts_%28variant%29.svg @@ -0,0 +1,806 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/Seal_of_the_State_of_Hawaii.svg b/apps/rave-mark/frontend/public/seals/Seal_of_the_State_of_Hawaii.svg new file mode 100644 index 0000000000..67b1818a35 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Seal_of_the_State_of_Hawaii.svg @@ -0,0 +1,3938 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/SouthDakota-StateSeal.svg b/apps/rave-mark/frontend/public/seals/SouthDakota-StateSeal.svg new file mode 100644 index 0000000000..c2bb10ae0d --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/SouthDakota-StateSeal.svg @@ -0,0 +1,5382 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/rave-mark/frontend/public/seals/State_Seal_of_Alaska.svg b/apps/rave-mark/frontend/public/seals/State_Seal_of_Alaska.svg new file mode 100644 index 0000000000..2744a24994 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/State_Seal_of_Alaska.svg @@ -0,0 +1,7795 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Utah-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Utah-StateSeal.svg new file mode 100644 index 0000000000..0d8820fe63 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Utah-StateSeal.svg @@ -0,0 +1,2133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Vermont_state_seal.svg b/apps/rave-mark/frontend/public/seals/Vermont_state_seal.svg new file mode 100644 index 0000000000..a5e05674f4 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Vermont_state_seal.svg @@ -0,0 +1,1621 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VERMONT + REEDOM + F + U + + NITY + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Virginia-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Virginia-StateSeal.svg new file mode 100644 index 0000000000..b266c0a48a --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Virginia-StateSeal.svg @@ -0,0 +1,1320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Washington-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Washington-StateSeal.svg new file mode 100644 index 0000000000..6fa2494fe4 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Washington-StateSeal.svg @@ -0,0 +1,2967 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/WestVirginia-StateSeal.svg b/apps/rave-mark/frontend/public/seals/WestVirginia-StateSeal.svg new file mode 100644 index 0000000000..206e227e17 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/WestVirginia-StateSeal.svg @@ -0,0 +1,6650 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Wisconsin-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Wisconsin-StateSeal.svg new file mode 100644 index 0000000000..fad0989ad5 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Wisconsin-StateSeal.svg @@ -0,0 +1,2101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/Wyoming-StateSeal.svg b/apps/rave-mark/frontend/public/seals/Wyoming-StateSeal.svg new file mode 100644 index 0000000000..56e770851d --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/Wyoming-StateSeal.svg @@ -0,0 +1,2225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/state-of-hamilton-official-seal.svg b/apps/rave-mark/frontend/public/seals/state-of-hamilton-official-seal.svg new file mode 100644 index 0000000000..bd4b01be10 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/state-of-hamilton-official-seal.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/rave-mark/frontend/public/seals/vermont-state-seal-bw.svg b/apps/rave-mark/frontend/public/seals/vermont-state-seal-bw.svg new file mode 100644 index 0000000000..6e9f2da5e1 --- /dev/null +++ b/apps/rave-mark/frontend/public/seals/vermont-state-seal-bw.svg @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/apps/rave-mark/frontend/script/build-stubs b/apps/rave-mark/frontend/script/build-stubs new file mode 120000 index 0000000000..8af0a46783 --- /dev/null +++ b/apps/rave-mark/frontend/script/build-stubs @@ -0,0 +1 @@ +../../../../script/build-stubs \ No newline at end of file diff --git a/apps/rave-mark/frontend/src/__snapshots__/app.test.tsx.snap b/apps/rave-mark/frontend/src/__snapshots__/app.test.tsx.snap deleted file mode 100644 index 3ed10d7711..0000000000 --- a/apps/rave-mark/frontend/src/__snapshots__/app.test.tsx.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders 1`] = ` -
-

- Hello RAVE -

-
-`; diff --git a/apps/rave-mark/frontend/src/api.ts b/apps/rave-mark/frontend/src/api.ts index 6a43246747..5a79b40a84 100644 --- a/apps/rave-mark/frontend/src/api.ts +++ b/apps/rave-mark/frontend/src/api.ts @@ -1,13 +1,21 @@ -import { type Api } from '@votingworks/rave-mark-backend'; -import React from 'react'; +import { AuthStatus, type Api } from '@votingworks/rave-mark-backend'; +import React, { useEffect } from 'react'; import * as grout from '@votingworks/grout'; -import { QueryClient } from '@tanstack/react-query'; +import { + QueryClient, + QueryKey, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import { QUERY_CLIENT_DEFAULT_OPTIONS } from '@votingworks/ui'; export type ApiClient = grout.Client; +const BASE_URL = '/api'; + export function createApiClient(): ApiClient { - return grout.createClient({ baseUrl: '/api' }); + return grout.createClient({ baseUrl: BASE_URL }); } export const ApiClientContext = React.createContext( @@ -25,3 +33,128 @@ export function useApiClient(): ApiClient { export function createQueryClient(): QueryClient { return new QueryClient({ defaultOptions: QUERY_CLIENT_DEFAULT_OPTIONS }); } + +export const getAuthStatus = { + queryKey(): QueryKey { + return ['getAuthStatus']; + }, + useQuery() { + const apiClient = useApiClient(); + const queryClient = useQueryClient(); + + useEffect(() => { + const eventSource = new EventSource( + grout.methodUrl('watchAuthStatus', BASE_URL) + ); + + eventSource.addEventListener('message', (event) => { + const authStatus = grout.deserialize(event.data) as AuthStatus; + queryClient.setQueryData(getAuthStatus.queryKey(), authStatus); + }); + + eventSource.addEventListener('error', (event) => { + // eslint-disable-next-line no-console + console.error('Error from watchAuthStatus event source', event); + }); + + return () => { + eventSource.close(); + }; + }, [queryClient]); + + return useQuery(this.queryKey(), () => apiClient.getAuthStatus(), { + staleTime: Infinity, + }); + }, +} as const; + +export const getVoterStatus = { + queryKey(): QueryKey { + return ['getVoterStatus']; + }, + useQuery() { + const apiClient = useApiClient(); + return useQuery( + this.queryKey(), + async () => (await apiClient.getVoterStatus()) ?? null, + { + staleTime: 0, + } + ); + }, +} as const; + +export const getElectionConfiguration = { + queryKey(): QueryKey { + return ['getElectionConfiguration']; + }, + useQuery() { + const apiClient = useApiClient(); + return useQuery( + this.queryKey(), + async () => (await apiClient.getElectionConfiguration()) ?? null, + { staleTime: 0 } + ); + }, +} as const; + +export const checkPin = { + useMutation() { + const apiClient = useApiClient(); + const queryClient = useQueryClient(); + return useMutation(apiClient.checkPin, { + async onSuccess() { + await queryClient.invalidateQueries(); + }, + }); + }, +} as const; + +export const createVoterRegistration = { + useMutation() { + const apiClient = useApiClient(); + const queryClient = useQueryClient(); + return useMutation(apiClient.createVoterRegistration, { + async onSuccess() { + await queryClient.invalidateQueries(); + }, + }); + }, +} as const; + +export const castBallot = { + useMutation() { + const apiClient = useApiClient(); + const queryClient = useQueryClient(); + return useMutation(apiClient.castBallot, { + async onSuccess() { + await queryClient.invalidateQueries(); + }, + }); + }, +} as const; + +export const sync = { + useMutation() { + const apiClient = useApiClient(); + const queryClient = useQueryClient(); + return useMutation(apiClient.sync, { + async onSuccess() { + await queryClient.invalidateQueries(); + }, + }); + }, +} as const; + +export const getServerSyncStatus = { + queryKey(): QueryKey { + return ['getServerSyncStatus']; + }, + useQuery() { + const apiClient = useApiClient(); + return useQuery(this.queryKey(), () => apiClient.getServerSyncStatus(), { + staleTime: 0, + refetchInterval: 1000, + }); + }, +} as const; diff --git a/apps/rave-mark/frontend/src/app.test.tsx b/apps/rave-mark/frontend/src/app.test.tsx deleted file mode 100644 index 3e0ac86e56..0000000000 --- a/apps/rave-mark/frontend/src/app.test.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { App } from './app'; - -test('renders', () => { - const { container } = render(); - expect(container).toMatchSnapshot(); -}); diff --git a/apps/rave-mark/frontend/src/app.tsx b/apps/rave-mark/frontend/src/app.tsx index 73c94c4fa6..70577cb4f1 100644 --- a/apps/rave-mark/frontend/src/app.tsx +++ b/apps/rave-mark/frontend/src/app.tsx @@ -1,19 +1,19 @@ -import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { LogSource, Logger } from '@votingworks/logging'; import { ColorMode, ScreenType, SizeMode } from '@votingworks/types'; import { AppBase, ErrorBoundary, H1, P, Prose } from '@votingworks/ui'; -import { BrowserRouter } from 'react-router-dom'; import { ApiClient, ApiClientContext, createApiClient, createQueryClient, } from './api'; +import { AppRoot } from './app_root'; +import { DisplaySettingsManager } from './components/display_settings_manager'; const DEFAULT_COLOR_MODE: ColorMode = 'contrastMedium'; const DEFAULT_SCREEN_TYPE: ScreenType = 'elo15'; -const DEFAULT_SIZE_MODE: SizeMode = 'm'; +const DEFAULT_SIZE_MODE: SizeMode = 'touchSmall'; export interface Props { logger?: Logger; @@ -34,22 +34,21 @@ export function App({ isTouchscreen screenType={DEFAULT_SCREEN_TYPE} > - - -

Something went wrong

-

Ask a poll worker to restart the ballot marking device.

- - } - > - - -

Hello RAVE

-
-
-
-
+ +

Something went wrong

+

Ask a poll worker to restart the ballot marking device.

+ + } + > + + + + + + +
); } diff --git a/apps/rave-mark/frontend/src/app_root.tsx b/apps/rave-mark/frontend/src/app_root.tsx new file mode 100644 index 0000000000..85d93fdf42 --- /dev/null +++ b/apps/rave-mark/frontend/src/app_root.tsx @@ -0,0 +1,23 @@ +import { throwIllegalValue } from '@votingworks/basics'; +import { AuthStatus } from '@votingworks/rave-mark-backend'; +import { getAuthStatus } from './api'; +import { HasCardScreen } from './screens/has_card_screen'; +import { NoCardScreen } from './screens/no_card_screen'; + +export function AppRoot(): JSX.Element { + const authStatusQuery = getAuthStatus.useQuery(); + const authStatus: AuthStatus = authStatusQuery.isSuccess + ? authStatusQuery.data + : { status: 'no_card' }; + + switch (authStatus.status) { + case 'no_card': + return ; + + case 'has_card': + return ; + + default: + throwIllegalValue(authStatus); + } +} diff --git a/apps/rave-mark/frontend/src/components/button_footer.tsx b/apps/rave-mark/frontend/src/components/button_footer.tsx new file mode 100644 index 0000000000..2673389ce6 --- /dev/null +++ b/apps/rave-mark/frontend/src/components/button_footer.tsx @@ -0,0 +1,13 @@ +/* stylelint-disable order/properties-order */ +import styled from 'styled-components'; + +export const ButtonFooter = styled.nav` + align-items: stretch; + border-top: ${(p) => p.theme.sizes.bordersRem.thick}rem solid + ${(p) => p.theme.colors.foreground}; + display: flex; + gap: 0.5rem; + justify-content: right; + min-height: 4.5rem; + padding: 0.5rem; +`; diff --git a/apps/rave-mark/frontend/src/components/display_settings_button.tsx b/apps/rave-mark/frontend/src/components/display_settings_button.tsx new file mode 100644 index 0000000000..0ff6c984c7 --- /dev/null +++ b/apps/rave-mark/frontend/src/components/display_settings_button.tsx @@ -0,0 +1,28 @@ +import { Button, Icons } from '@votingworks/ui'; +import styled from 'styled-components'; + +const LabelContainer = styled.span` + align-items: center; + display: flex; + flex-wrap: nowrap; + font-weight: ${(p) => p.theme.sizes.fontWeight.semiBold}; + gap: 0.5rem; + text-align: left; +`; + +export interface DisplaySettingsButtonProps { + onPress(): void; +} + +export function DisplaySettingsButton({ + onPress, +}: DisplaySettingsButtonProps): JSX.Element { + return ( + + ); +} diff --git a/apps/rave-mark/frontend/src/components/display_settings_manager.tsx b/apps/rave-mark/frontend/src/components/display_settings_manager.tsx new file mode 100644 index 0000000000..022f7cb185 --- /dev/null +++ b/apps/rave-mark/frontend/src/components/display_settings_manager.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { ThemeManagerContext, useQueryChangeListener } from '@votingworks/ui'; +import { DefaultTheme, ThemeContext } from 'styled-components'; +import { getAuthStatus } from '../api'; + +/** + * Side-effect component for monitoring for auth and voter session changes and + * resetting/restoring voter display settings as needed. + */ +export function DisplaySettingsManager(): JSX.Element | null { + const themeManager = React.useContext(ThemeManagerContext); + const currentTheme = React.useContext(ThemeContext); + + const authStatusQuery = getAuthStatus.useQuery(); + + const [voterSessionTheme, setVoterSessionTheme] = + React.useState(null); + + useQueryChangeListener(authStatusQuery, (newStatus, previousStatus) => { + // Reset to default theme when election official logs in: + if ( + previousStatus?.status === 'no_card' && + newStatus.status !== 'no_card' + ) { + setVoterSessionTheme(currentTheme); + themeManager.resetThemes(); + } + + // Reset to previous voter settings when election official logs out: + if ( + previousStatus?.status !== 'no_card' && + newStatus.status === 'no_card' && + voterSessionTheme + ) { + themeManager.setColorMode(voterSessionTheme.colorMode); + themeManager.setSizeMode(voterSessionTheme.sizeMode); + setVoterSessionTheme(null); + } + }); + + return null; +} diff --git a/apps/rave-mark/frontend/src/components/pin_pad_modal.test.tsx b/apps/rave-mark/frontend/src/components/pin_pad_modal.test.tsx new file mode 100644 index 0000000000..dd0fc01e00 --- /dev/null +++ b/apps/rave-mark/frontend/src/components/pin_pad_modal.test.tsx @@ -0,0 +1,148 @@ +import { PinLength, renderWithThemes as render } from '@votingworks/ui'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { PinPadModal } from './pin_pad_modal'; + +const pinLength = PinLength.exactly(8); + +test('shows errors if provided', async () => { + const onEnter = jest.fn(); + const onDismiss = jest.fn(); + + const { rerender } = render( + + ); + + await screen.findByText('Error: Invalid PIN'); + + rerender( + + ); + + expect(screen.queryByText(/Error:/)).not.toBeInTheDocument(); +}); + +test('calls onEnter with the PIN when the enter button is pressed', async () => { + const onEnter = jest.fn(); + const onDismiss = jest.fn(); + + render( + + ); + + userEvent.click(await screen.findByRole('button', { name: 'enter' })); + expect(onEnter).toHaveBeenCalledWith(''); + + userEvent.click(screen.getByRole('button', { name: '1' })); + userEvent.click(screen.getByRole('button', { name: '2' })); + userEvent.click(screen.getByRole('button', { name: '3' })); + userEvent.click(screen.getByRole('button', { name: '4' })); + + userEvent.click(await screen.findByRole('button', { name: 'enter' })); + expect(onEnter).toHaveBeenCalledWith('1234'); +}); + +test('calls onDismiss when the dismiss button is pressed', async () => { + const onEnter = jest.fn(); + const onDismiss = jest.fn(); + + render( + + ); + + userEvent.click(await screen.findByRole('button', { name: 'cancel' })); + expect(onDismiss).toHaveBeenCalled(); +}); + +test('displays the masked PIN', async () => { + const onEnter = jest.fn(); + const onDismiss = jest.fn(); + + render( + + ); + + userEvent.click(screen.getByRole('button', { name: '1' })); + userEvent.click(screen.getByRole('button', { name: '1' })); + userEvent.click(screen.getByRole('button', { name: '1' })); + userEvent.click(screen.getByRole('button', { name: '1' })); + + await screen.findByText('• • • • - - - -'); +}); + +test('clears the PIN when the clear button is pressed', async () => { + const onEnter = jest.fn(); + const onDismiss = jest.fn(); + + render( + + ); + + userEvent.click(screen.getByRole('button', { name: '1' })); + userEvent.click(screen.getByRole('button', { name: '2' })); + userEvent.click(screen.getByRole('button', { name: '3' })); + userEvent.click(screen.getByRole('button', { name: '4' })); + userEvent.click(screen.getByRole('button', { name: 'clear' })); + + await screen.findByText('- - - - - - - -'); +}); + +test('removes the last digit when the backspace button is pressed', async () => { + const onEnter = jest.fn(); + const onDismiss = jest.fn(); + + render( + + ); + + userEvent.click(screen.getByRole('button', { name: '1' })); + userEvent.click(screen.getByRole('button', { name: '2' })); + userEvent.click(screen.getByRole('button', { name: '3' })); + userEvent.click(screen.getByRole('button', { name: '4' })); + userEvent.click(screen.getByRole('button', { name: 'backspace' })); + + await screen.findByText('• • • - - - - -'); +}); diff --git a/apps/rave-mark/frontend/src/components/pin_pad_modal.tsx b/apps/rave-mark/frontend/src/components/pin_pad_modal.tsx new file mode 100644 index 0000000000..57acada649 --- /dev/null +++ b/apps/rave-mark/frontend/src/components/pin_pad_modal.tsx @@ -0,0 +1,101 @@ +import { + Button, + Modal, + NumberPad, + PinLength, + Text, + usePinEntry, +} from '@votingworks/ui'; +import React from 'react'; +import styled from 'styled-components'; + +const NumberPadWrapper = styled.div` + display: flex; + justify-content: center; + margin-top: 10px; + font-size: 1em; + > div { + width: 400px; + } + *:focus { + outline: none; + } +`; + +const EnteredCode = styled.div` + margin-top: 5px; + text-align: center; + font-family: monospace; + font-size: 1.5em; + font-weight: 600; +`; + +export interface PinPadModalProps { + pinLength: PinLength; + primaryButtonLabel: string; + dismissButtonLabel: string; + onEnter: (pin: string) => void; + onDismiss: () => void; + title?: string; + disabled?: boolean; + error?: string; +} + +export function PinPadModal({ + pinLength, + primaryButtonLabel, + dismissButtonLabel, + onEnter, + onDismiss, + title = 'Enter Your PIN', + disabled, + error, +}: PinPadModalProps): JSX.Element { + const pinEntry = usePinEntry({ pinLength }); + + function handleEnter() { + onEnter(pinEntry.current); + } + + function dismissPinModal() { + pinEntry.reset(); + onDismiss(); + } + + return ( + + {error && Error: {error}} + {pinEntry.display} + + + + + } + actions={ + + + + + } + /> + ); +} diff --git a/apps/rave-mark/frontend/src/components/text_input.tsx b/apps/rave-mark/frontend/src/components/text_input.tsx new file mode 100644 index 0000000000..1a4019f4db --- /dev/null +++ b/apps/rave-mark/frontend/src/components/text_input.tsx @@ -0,0 +1,144 @@ +import { + Button, + Modal, + P, + TouchTextInput, + US_ENGLISH_ALNUM_KEYMAP, + VirtualKeyboard, +} from '@votingworks/ui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +interface Props { + disabled?: boolean; +} + +/** + * A container for the text input, i.e. the background and border. + */ +export const TextInputContainer = styled.div` + display: inline-block; + border: 1px solid #cccccc; + background: ${({ disabled = false }) => (disabled ? '#dddddd' : '#ffffff')}; + width: 100%; + padding: 0.35rem 0.5rem; + line-height: 1.25; + height: 2.25em; +`; + +/** + * A small text input label that appears above the text input. + */ +export const TextInputLabel = styled.label` + display: block; + font-size: 0.55rem; + font-weight: 500; + margin-bottom: 0; + margin-top: -0.25rem; + color: #666666; +`; + +export interface TextInputProps { + label: string; + value?: string; + disabled?: boolean; + onChange?: (value: string) => void; + 'data-testid'?: string; +} + +/** + * A text input that opens a virtual keyboard when clicked. + */ +export function TextInput({ + label, + value: valueFromProps = '', + disabled, + onChange, + 'data-testid': testId, +}: TextInputProps): JSX.Element { + const [value, setValue] = useState(valueFromProps); + const [isEditing, setIsEditing] = useState(false); + + function onClick() { + setIsEditing(true); + } + + function onKeyboardBackspace() { + setValue((prev) => prev.slice(0, -1)); + } + + function onKeyboardKeyPress(key: string) { + setValue((prev) => prev + key); + } + + function onAccept() { + onChange?.(value); + setIsEditing(false); + } + + function onCancel() { + setValue(valueFromProps); + setIsEditing(false); + } + + function keyDisabled() { + return false; + } + + return ( + + {isEditing && ( + + +

 

+ +
+ } + actions={ + + + + + } + /> + )} + + {label} + {value} + + + ); +} + +export const InlineForm = styled.div` + display: flex; + flex-direction: row; + input[type='text'] { + flex: 1; + border-radius: 0.25em; + } + & > button:not(:last-child), + & > input[type='text']:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + & > button:not(:first-child), + & > input[type='text']:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +`; diff --git a/apps/rave-mark/frontend/src/env.d.ts b/apps/rave-mark/frontend/src/env.d.ts new file mode 100644 index 0000000000..cf96345d80 --- /dev/null +++ b/apps/rave-mark/frontend/src/env.d.ts @@ -0,0 +1,6 @@ +declare namespace NodeJS { + export interface ProcessEnv { + IS_INTEGRATION_TEST?: string; + REACT_APP_BALLOT_PRINTER?: string; + } +} diff --git a/apps/rave-mark/frontend/src/globals.ts b/apps/rave-mark/frontend/src/globals.ts new file mode 100644 index 0000000000..f9f8112a70 --- /dev/null +++ b/apps/rave-mark/frontend/src/globals.ts @@ -0,0 +1,3 @@ +import { PinLength } from '@votingworks/ui'; + +export const COMMON_ACCESS_CARD_PIN_LENGTH = PinLength.range(6, 8); diff --git a/apps/rave-mark/frontend/src/index.tsx b/apps/rave-mark/frontend/src/index.tsx index b00df0ad0b..5250b114f1 100644 --- a/apps/rave-mark/frontend/src/index.tsx +++ b/apps/rave-mark/frontend/src/index.tsx @@ -1,13 +1,5 @@ import './polyfills'; -import React from 'react'; -import ReactDom from 'react-dom'; -import { DevDock } from '@votingworks/dev-dock-frontend'; +import { createRoot } from 'react-dom/client'; import { App } from './app'; -ReactDom.render( - - - - , - document.getElementById('root') as HTMLElement -); +createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/apps/rave-mark/frontend/src/screens/__snapshots__/no_card_screen.test.tsx.snap b/apps/rave-mark/frontend/src/screens/__snapshots__/no_card_screen.test.tsx.snap new file mode 100644 index 0000000000..3a78f33efa --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/__snapshots__/no_card_screen.test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` +
+
+
+

+ Welcome +

+

+ Insert your Common Access Card to begin. +

+
+
+
+`; diff --git a/apps/rave-mark/frontend/src/screens/admin/dashboard_screen.tsx b/apps/rave-mark/frontend/src/screens/admin/dashboard_screen.tsx new file mode 100644 index 0000000000..cb81cac07f --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/admin/dashboard_screen.tsx @@ -0,0 +1,138 @@ +import { + Button, + H1, + H2, + Main, + Screen, + TD, + TH, + Table, + Text, +} from '@votingworks/ui'; +import React from 'react'; +import styled from 'styled-components'; +import { format } from '@votingworks/utils'; +import { getServerSyncStatus, sync } from '../../api'; + +export interface DashboardScreenProps { + onClickShowVoterFlow: () => void; +} + +const FloatingTopRight = styled.div` + position: absolute; + top: 0; + right: 0; + padding: 1rem; + display: flex; + flex-direction: row; + gap: 1rem; +`; + +export function DashboardScreen({ + onClickShowVoterFlow, +}: DashboardScreenProps): JSX.Element { + const syncMutation = sync.useMutation(); + const getServerSyncStatusQuery = getServerSyncStatus.useQuery(); + + return ( + +
+

Admin

+ + + + + +

Sync Status

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TypeSynced with ServerPending Sync to Server
Ballots Cast + {format.count( + getServerSyncStatusQuery.data?.status.printedBallots.synced ?? + 0 + )} + + {format.count( + getServerSyncStatusQuery.data?.status.printedBallots + .pending ?? 0 + )} +
Registration Requests + {format.count( + getServerSyncStatusQuery.data?.status.pendingRegistrations + .synced ?? 0 + )} + + {format.count( + getServerSyncStatusQuery.data?.status.pendingRegistrations + .pending ?? 0 + )} +
Elections + {format.count( + getServerSyncStatusQuery.data?.status.elections.synced ?? 0 + )} + n/a
+ +

Sync History

+ + + + + + + + + + + {getServerSyncStatusQuery.data?.attempts.map((attempt) => ( + + + + + + + ))} + +
Started ByStatusTriggerCompleted
{attempt.creator} + + {attempt.statusMessage.split('\n').map((line, i, lines) => ( + + {line} + {i < lines.length - 1 &&
} +
+ ))} +
+
{attempt.trigger}{attempt.completedAt?.toRelative()}
+
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/admin/index.tsx b/apps/rave-mark/frontend/src/screens/admin/index.tsx new file mode 100644 index 0000000000..353460fbb2 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/admin/index.tsx @@ -0,0 +1 @@ +export * from './dashboard_screen'; diff --git a/apps/rave-mark/frontend/src/screens/admin_flow_screen.tsx b/apps/rave-mark/frontend/src/screens/admin_flow_screen.tsx new file mode 100644 index 0000000000..2ab34c9274 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/admin_flow_screen.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; +import { VoterFlowScreen } from './voter_flow_screen'; +import * as Admin from './admin'; + +export function AdminFlowScreen(): JSX.Element { + const [showVoterFlow, setShowVoterFlow] = useState(false); + + if (showVoterFlow) { + return ; + } + + return ( + setShowVoterFlow(true)} + /> + ); +} diff --git a/apps/rave-mark/frontend/src/screens/has_card_screen.tsx b/apps/rave-mark/frontend/src/screens/has_card_screen.tsx new file mode 100644 index 0000000000..1f56cfd8bf --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/has_card_screen.tsx @@ -0,0 +1,14 @@ +import { getVoterStatus } from '../api'; +import { AdminFlowScreen } from './admin_flow_screen'; +import { VoterFlowScreen } from './voter_flow_screen'; + +export function HasCardScreen(): JSX.Element | null { + const getVoterStatusQuery = getVoterStatus.useQuery(); + const voterStatus = getVoterStatusQuery.data; + + if (!voterStatus) { + return null; + } + + return voterStatus.isAdmin ? : ; +} diff --git a/apps/rave-mark/frontend/src/screens/no_card_screen.test.tsx b/apps/rave-mark/frontend/src/screens/no_card_screen.test.tsx new file mode 100644 index 0000000000..79b121c4fe --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/no_card_screen.test.tsx @@ -0,0 +1,7 @@ +import { renderWithThemes as render } from '@votingworks/ui'; +import { NoCardScreen } from './no_card_screen'; + +test('renders', () => { + const { container } = render(); + expect(container).toMatchSnapshot(); +}); diff --git a/apps/rave-mark/frontend/src/screens/no_card_screen.tsx b/apps/rave-mark/frontend/src/screens/no_card_screen.tsx new file mode 100644 index 0000000000..fe5a59c12c --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/no_card_screen.tsx @@ -0,0 +1,12 @@ +import { H1, Main, P, Screen } from '@votingworks/ui'; + +export function NoCardScreen(): JSX.Element { + return ( + +
+

Welcome

+

Insert your Common Access Card to begin.

+
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/registration/index.tsx b/apps/rave-mark/frontend/src/screens/registration/index.tsx new file mode 100644 index 0000000000..424466a954 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/registration/index.tsx @@ -0,0 +1,2 @@ +export * from './start_screen'; +export * from './status_screen'; diff --git a/apps/rave-mark/frontend/src/screens/registration/start_screen.tsx b/apps/rave-mark/frontend/src/screens/registration/start_screen.tsx new file mode 100644 index 0000000000..5339cf958d --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/registration/start_screen.tsx @@ -0,0 +1,143 @@ +import { extractErrorMessage } from '@votingworks/basics'; +import { Button, H1, Main, P, Screen } from '@votingworks/ui'; +import { useState } from 'react'; +import { createVoterRegistration, getAuthStatus } from '../../api'; +import { PinPadModal } from '../../components/pin_pad_modal'; +import { InlineForm, TextInput } from '../../components/text_input'; +import { COMMON_ACCESS_CARD_PIN_LENGTH } from '../../globals'; + +export function StartScreen(): JSX.Element { + const authStatusQuery = getAuthStatus.useQuery(); + const cardDetails = + authStatusQuery.data?.status === 'has_card' + ? authStatusQuery.data.card + : undefined; + const [givenName, setGivenName] = useState(cardDetails?.givenName ?? ''); + const [familyName, setFamilyName] = useState(cardDetails?.familyName ?? ''); + const [addressLine1, setAddressLine1] = useState(''); + const [addressLine2, setAddressLine2] = useState(''); + const [city, setCity] = useState(''); + const [state, setState] = useState(''); + const [postalCode, setPostalCode] = useState(''); + const [stateId, setStateId] = useState(''); + const [isShowingPinModal, setIsShowingPinModal] = useState(false); + const createVoterRegistrationMutation = createVoterRegistration.useMutation(); + const error = createVoterRegistrationMutation.data?.err(); + + function onSubmitRegistrationForm() { + setIsShowingPinModal(true); + } + + function onEnterPin(pin: string) { + createVoterRegistrationMutation.mutate({ + givenName, + familyName, + addressLine1, + addressLine2, + city, + state, + postalCode, + stateId, + pin, + }); + } + + return ( + +
+

Registration

+

Fill out your information to register to vote.

+ + { + setGivenName(newValue); + }} + /> + { + setFamilyName(newValue); + }} + /> + + + { + setAddressLine1(newValue); + }} + /> + + + { + setAddressLine2(newValue); + }} + /> + + + { + setCity(newValue); + }} + /> + { + setState(newValue); + }} + /> + { + setPostalCode(newValue); + }} + /> + + + { + setStateId(newValue); + }} + /> + + + {isShowingPinModal && ( + setIsShowingPinModal(false)} + disabled={createVoterRegistrationMutation.isLoading} + error={error ? extractErrorMessage(error) : undefined} + /> + )} +
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/registration/status_screen.tsx b/apps/rave-mark/frontend/src/screens/registration/status_screen.tsx new file mode 100644 index 0000000000..46845c558d --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/registration/status_screen.tsx @@ -0,0 +1,12 @@ +import { H1, Main, P, Screen } from '@votingworks/ui'; + +export function StatusScreen(): JSX.Element { + return ( + +
+

Registration

+

Registration is pending.

+
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voter_flow_screen.tsx b/apps/rave-mark/frontend/src/screens/voter_flow_screen.tsx new file mode 100644 index 0000000000..a71ccf7544 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voter_flow_screen.tsx @@ -0,0 +1,332 @@ +import { assert, find, throwIllegalValue } from '@votingworks/basics'; +import { + ContestId, + OptionalVote, + VotesDict, + getContests, +} from '@votingworks/types'; +import { useState } from 'react'; +import { getElectionConfiguration, getVoterStatus } from '../api'; +import * as Registration from './registration'; +import * as Voting from './voting'; + +interface InitState { + type: 'init'; +} + +interface MarkState { + type: 'mark'; + contestIndex: number; + votes: VotesDict; +} + +interface ReviewOnscreenState { + type: 'review_onscreen'; + votes: VotesDict; + contestIndex?: number; +} + +interface PrintBallotState { + type: 'print_ballot'; + votes: VotesDict; +} + +interface ReviewPrintedBallotState { + type: 'review_printed'; + votes: VotesDict; +} + +interface SubmitState { + type: 'submit'; + votes: VotesDict; +} + +interface PostVoteState { + type: 'post_vote'; +} + +type VoterFlowState = + | InitState + | MarkState + | ReviewOnscreenState + | PrintBallotState + | ReviewPrintedBallotState + | SubmitState + | PostVoteState; + +interface RegisteredStateScreenProps { + onIsVotingSessionInProgressChanged: ( + isVotingSessionInProgress: boolean + ) => void; +} + +function RegisteredStateScreen({ + onIsVotingSessionInProgressChanged, +}: RegisteredStateScreenProps): JSX.Element | null { + const getElectionConfigurationQuery = getElectionConfiguration.useQuery(); + const electionConfiguration = getElectionConfigurationQuery.data; + const [voterFlowState, setVoterFlowState] = useState({ + type: 'init', + }); + + if (!electionConfiguration) { + return null; + } + + const { electionDefinition, ballotStyleId, precinctId } = + electionConfiguration; + const ballotStyle = find( + electionDefinition.election.ballotStyles, + (bs) => bs.id === ballotStyleId + ); + const contests = getContests({ + election: electionDefinition.election, + ballotStyle, + }); + + function onStartVoting() { + onIsVotingSessionInProgressChanged(true); + setVoterFlowState((prev) => { + assert(prev?.type === 'init'); + return { + type: 'mark', + contestIndex: 0, + votes: {}, + }; + }); + } + + function onReviewContestAtIndex(contestIndex: number) { + setVoterFlowState((prev) => { + assert(prev?.type === 'review_onscreen'); + return { + ...prev, + contestIndex, + }; + }); + } + + function onReviewConfirm() { + setVoterFlowState((prev) => { + assert(prev?.type === 'review_onscreen'); + return { + type: 'print_ballot', + votes: prev.votes, + }; + }); + } + + function updateVote(contestId: ContestId, vote: OptionalVote) { + setVoterFlowState((prev) => { + assert(prev?.type === 'mark' || prev?.type === 'review_onscreen'); + return { + ...prev, + votes: { + ...prev.votes, + [contestId]: vote, + }, + }; + }); + } + + function goMarkNext() { + setVoterFlowState((prev) => { + assert(prev?.type === 'mark'); + if (prev.contestIndex === contests.length - 1) { + return { + type: 'review_onscreen', + votes: prev.votes, + }; + } + + return { + ...prev, + contestIndex: prev.contestIndex + 1, + }; + }); + } + + function onReturnToReview() { + setVoterFlowState((prev) => { + assert(prev?.type === 'review_onscreen'); + return { + type: 'review_onscreen', + votes: prev.votes, + }; + }); + } + + function goMarkPrevious() { + setVoterFlowState((prev) => { + assert(prev?.type === 'mark'); + return { + ...prev, + contestIndex: Math.max(0, prev.contestIndex - 1), + }; + }); + } + + function onPrintBallotCompleted() { + setVoterFlowState((prev) => { + assert(prev?.type === 'print_ballot'); + return { + type: 'review_printed', + votes: prev.votes, + }; + }); + } + + function onConfirmPrintedBallotSelections() { + setVoterFlowState((prev) => { + assert(prev?.type === 'review_printed'); + return { + type: 'submit', + votes: prev.votes, + }; + }); + } + + function onRejectPrintedBallotSelections() { + setVoterFlowState((prev) => { + assert(prev?.type === 'review_printed'); + return { + type: 'review_onscreen', + votes: prev.votes, + }; + }); + } + + function onSubmitted() { + setVoterFlowState((prev) => { + assert(prev?.type === 'submit'); + return { + type: 'post_vote', + votes: prev.votes, + }; + }); + } + + switch (voterFlowState.type) { + case 'init': + return ( + + ); + + case 'mark': + return ( + + ); + + case 'review_onscreen': + if (typeof voterFlowState.contestIndex === 'number') { + return ( + + ); + } + + return ( + + ); + + case 'print_ballot': + return ( + ''} + // TODO: use live vs test mode? + isLiveMode={false} + onPrintCompleted={onPrintBallotCompleted} + /> + ); + + case 'review_printed': + return ( + + ); + + case 'submit': + return ( + + ); + + case 'post_vote': + return ; + + default: + throwIllegalValue(voterFlowState); + } +} + +export function VoterFlowScreen(): JSX.Element | null { + const [isVotingSessionInProgress, setIsVotingSessionInProgress] = + useState(false); + const getVoterStatusQuery = getVoterStatus.useQuery(); + const voterStatus = getVoterStatusQuery.data; + + if (!voterStatus) { + return null; + } + + if (isVotingSessionInProgress) { + return ( + + ); + } + + switch (voterStatus.status) { + case 'unregistered': + return ; + + case 'registration_pending': + return ; + + case 'registered': + return ( + + ); + + case 'voted': + return ; + + default: + throwIllegalValue(voterStatus.status); + } +} diff --git a/apps/rave-mark/frontend/src/screens/voting/already_voted_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/already_voted_screen.tsx new file mode 100644 index 0000000000..8577905949 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/already_voted_screen.tsx @@ -0,0 +1,15 @@ +import { H2, Main, Screen, Text } from '@votingworks/ui'; + +export function AlreadyVotedScreen(): JSX.Element { + return ( + +
+

You’ve Already Voted

+ + If you haven’t already done so, please mail your paper ballot using + the provided envelope and personalized mailing label. + +
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/index.tsx b/apps/rave-mark/frontend/src/screens/voting/index.tsx new file mode 100644 index 0000000000..6daf97e400 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/index.tsx @@ -0,0 +1,10 @@ +export * from './already_voted_screen'; +export * from './mark_screen'; +export * from './post_vote_screen'; +export * from './print_ballot_screen'; +export * from './print_mailing_label_screen'; +export * from './review_mark_screen'; +export * from './review_onscreen_ballot_screen'; +export * from './review_printed_ballot_screen'; +export * from './start_screen'; +export * from './submit_screen'; diff --git a/apps/rave-mark/frontend/src/screens/voting/mark_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/mark_screen.tsx new file mode 100644 index 0000000000..6811573a03 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/mark_screen.tsx @@ -0,0 +1,91 @@ +import { assert, throwIllegalValue } from '@votingworks/basics'; +import { Contest as MarkFlowContest } from '@votingworks/mark-flow-ui'; +import { + ContestId, + Contests, + ElectionDefinition, + OptionalVote, + VotesDict, +} from '@votingworks/types'; +import { Button, DisplaySettings, Modal, Screen } from '@votingworks/ui'; +import { useState } from 'react'; +import { ButtonFooter } from '../../components/button_footer'; +import { DisplaySettingsButton } from '../../components/display_settings_button'; + +export interface MarkScreenProps { + electionDefinition: ElectionDefinition; + contests: Contests; + contestIndex: number; + votes: VotesDict; + updateVote: (contestId: ContestId, vote: OptionalVote) => void; + goNext?: () => void; + goPrevious?: () => void; +} + +function noop(): void { + // Do nothing +} + +export function MarkScreen({ + electionDefinition, + contests, + contestIndex, + votes, + updateVote, + goNext, + goPrevious, +}: MarkScreenProps): JSX.Element | null { + assert(contestIndex >= 0 && contestIndex < contests.length); + + const contest = contests[contestIndex]; + const hasFinishedVotingInThisContest = + contest.type === 'candidate' + ? (votes[contest.id]?.length ?? 0) === contest.seats + : contest.type === 'yesno' + ? votes[contest.id] !== undefined + : throwIllegalValue(contest); + const [isShowingDisplaySettings, setIsShowingDisplaySettings] = + useState(false); + + function onPressDisplaySettingsButton() { + setIsShowingDisplaySettings(true); + } + + function onCloseDisplaySettings() { + setIsShowingDisplaySettings(false); + } + + return ( + + {isShowingDisplaySettings && ( + } + /> + )} + + + + + + + + ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/post_vote_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/post_vote_screen.tsx new file mode 100644 index 0000000000..41dfcd14a1 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/post_vote_screen.tsx @@ -0,0 +1,22 @@ +import { H2, Main, Screen, Text } from '@votingworks/ui'; + +export function PostVoteScreen(): JSX.Element { + return ( + +
+

Mail Your Paper Ballot

+ + Please mail your paper ballot to: + + + 123 Main St. +
+ Anytown, USA 12345 +
+ + Remove your card when you’re ready to end your voting session. + +
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/print_ballot_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/print_ballot_screen.tsx new file mode 100644 index 0000000000..8e7dacceb7 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/print_ballot_screen.tsx @@ -0,0 +1,66 @@ +import { PrintPage as MarkFlowPrintPage } from '@votingworks/mark-flow-ui'; +import { + BallotStyleId, + ElectionDefinition, + PrecinctId, + PrintOptions, + VotesDict, +} from '@votingworks/types'; +import { printElement } from '@votingworks/ui'; +import { useEffect, useRef } from 'react'; + +export interface PrintBallotScreenProps { + electionDefinition: ElectionDefinition; + ballotStyleId: BallotStyleId; + precinctId: PrecinctId; + votes: VotesDict; + generateBallotId: () => string; + isLiveMode: boolean; + onPrintCompleted: () => void; +} + +export function PrintBallotScreen({ + electionDefinition, + ballotStyleId, + precinctId, + votes, + generateBallotId, + isLiveMode, + onPrintCompleted, +}: PrintBallotScreenProps): JSX.Element { + const printTimer = useRef(); + + useEffect(() => { + return () => { + window.clearTimeout(printTimer.current); + }; + }, []); + + function printElementToBallotPrinter( + element: JSX.Element, + printOptions: PrintOptions + ) { + return printElement(element, { + ...printOptions, + deviceName: process.env.REACT_APP_BALLOT_PRINTER, + }); + } + + return ( + { + printTimer.current = window.setTimeout( + onPrintCompleted, + process.env.IS_INTEGRATION_TEST === 'true' ? 500 : 5000 + ); + }} + /> + ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/print_mailing_label_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/print_mailing_label_screen.tsx new file mode 100644 index 0000000000..45772690a6 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/print_mailing_label_screen.tsx @@ -0,0 +1,14 @@ +import { H1, Main, PrintingBallotImage, Prose, Screen } from '@votingworks/ui'; + +export function PrintMailingLabelScreen(): JSX.Element { + return ( + +
+ + +

Printing Your Mailing Label…

+
+
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/review_mark_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/review_mark_screen.tsx new file mode 100644 index 0000000000..f53111dd1c --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/review_mark_screen.tsx @@ -0,0 +1,48 @@ +import { assert } from '@votingworks/basics'; +import { Contest as MarkFlowContest } from '@votingworks/mark-flow-ui'; +import { + ContestId, + Contests, + ElectionDefinition, + OptionalVote, + VotesDict, +} from '@votingworks/types'; +import { Button, Screen } from '@votingworks/ui'; +import { ButtonFooter } from '../../components/button_footer'; + +export interface ReviewMarkScreenProps { + electionDefinition: ElectionDefinition; + contests: Contests; + contestIndex: number; + votes: VotesDict; + updateVote: (contestId: ContestId, vote: OptionalVote) => void; + onReturnToReview: () => void; +} + +export function ReviewMarkScreen({ + electionDefinition, + contests, + contestIndex, + votes, + updateVote, + onReturnToReview, +}: ReviewMarkScreenProps): JSX.Element | null { + assert(contestIndex >= 0 && contestIndex < contests.length); + const contest = contests[contestIndex]; + + return ( + + + + + + + ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/review_onscreen_ballot_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/review_onscreen_ballot_screen.tsx new file mode 100644 index 0000000000..c459242e41 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/review_onscreen_ballot_screen.tsx @@ -0,0 +1,58 @@ +import { Review as MarkFlowReview } from '@votingworks/mark-flow-ui'; +import { Button, H1, Main, Screen, WithScrollButtons } from '@votingworks/ui'; +import styled from 'styled-components'; +import { + Contests, + ElectionDefinition, + PrecinctId, + VotesDict, +} from '@votingworks/types'; +import { ButtonFooter } from '../../components/button_footer'; + +const ContentHeader = styled.div` + padding: 0.5rem 0.75rem 0; +`; + +export interface ReviewOnscreenBallotScreenProps { + electionDefinition: ElectionDefinition; + contests: Contests; + votes: VotesDict; + precinctId: PrecinctId; + onConfirm: () => void; + goToIndex: (contestIndex: number) => void; +} + +export function ReviewOnscreenBallotScreen({ + electionDefinition, + contests, + votes, + precinctId, + onConfirm, + goToIndex, +}: ReviewOnscreenBallotScreenProps): JSX.Element { + return ( + +
+ +

Review Your Votes

+
+ + { + goToIndex(contests.findIndex((c) => c.id === contestId)); + }} + /> + +
+ + + +
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/review_printed_ballot_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/review_printed_ballot_screen.tsx new file mode 100644 index 0000000000..d58902b54d --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/review_printed_ballot_screen.tsx @@ -0,0 +1,34 @@ +import { Button, H1, Main, P, Screen } from '@votingworks/ui'; + +export interface ReviewPrintedBallotScreenProps { + onConfirm: () => void; + onReject: () => void; +} + +export function ReviewPrintedBallotScreen({ + onConfirm: onConfirmPrintedBallotSelections, + onReject: onRejectPrintedBallotSelections, +}: ReviewPrintedBallotScreenProps): JSX.Element | null { + return ( + +
+

Review Your Ballot

+

+ Please check your selections on the printed ballot paper, then select + one of the choices below. +

+

+ +

+

- or -

+

+ +

+
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/start_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/start_screen.tsx new file mode 100644 index 0000000000..eb23661006 --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/start_screen.tsx @@ -0,0 +1,29 @@ +import { ElectionDefinition } from '@votingworks/types'; +import { Button, H1, Main, Screen, Text } from '@votingworks/ui'; +import { formatShortDate } from '@votingworks/utils'; +import { DateTime } from 'luxon'; + +export interface StartScreenProps { + electionDefinition: ElectionDefinition; + onStartVoting: () => void; +} + +export function StartScreen({ + electionDefinition, + onStartVoting, +}: StartScreenProps): JSX.Element | null { + return ( + +
+

Ready to Vote

+ {electionDefinition.election.title} + + {formatShortDate(DateTime.fromISO(electionDefinition.election.date))} + + +
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/screens/voting/submit_screen.tsx b/apps/rave-mark/frontend/src/screens/voting/submit_screen.tsx new file mode 100644 index 0000000000..486380df0e --- /dev/null +++ b/apps/rave-mark/frontend/src/screens/voting/submit_screen.tsx @@ -0,0 +1,86 @@ +import { VotesDict } from '@votingworks/types'; +import { + Button, + H2, + Main, + P, + Prose, + Screen, + fontSizeTheme, +} from '@votingworks/ui'; +import { useState } from 'react'; +import { castBallot } from '../../api'; +import { PinPadModal } from '../../components/pin_pad_modal'; +import { COMMON_ACCESS_CARD_PIN_LENGTH } from '../../globals'; + +export interface SubmitScreenProps { + votes: VotesDict; + onSubmitted: () => void; +} + +export function SubmitScreen({ + votes, + onSubmitted, +}: SubmitScreenProps): JSX.Element { + const [pinError, setPinError] = useState(); + const [isShowingPinModal, setIsShowingPinModal] = useState(false); + const castBallotMutation = castBallot.useMutation(); + const castBallotMutationMutateAsync = castBallotMutation.mutateAsync; + const isCheckingPin = castBallotMutation.isLoading; + + async function submitBallot(pin: string) { + setPinError(undefined); + try { + await castBallotMutationMutateAsync( + { votes, pin }, + { + onError(err) { + setPinError(err instanceof Error ? err.message : `${err}`); + }, + } + ); + onSubmitted(); + } catch (err) { + setPinError(err instanceof Error ? err.message : `${err}`); + } + } + + async function handleEnter(pin: string) { + await submitBallot(pin); + } + + return ( + +
+ +

You’re Almost Done

+

+ Thanks for reviewing your printed ballot! +
+ Tap the button below to continue. +

+ + {isShowingPinModal && ( + setIsShowingPinModal(false)} + disabled={isCheckingPin} + error={pinError} + /> + )} +
+
+
+ ); +} diff --git a/apps/rave-mark/frontend/src/stubs/fs.ts b/apps/rave-mark/frontend/src/stubs/fs.ts new file mode 100644 index 0000000000..e43dbcf9f1 --- /dev/null +++ b/apps/rave-mark/frontend/src/stubs/fs.ts @@ -0,0 +1,299 @@ +/** Stub for 'fs' module, generated by script/build-stubs. DO NOT EDIT */ +/* eslint-disable */ +/* istanbul ignore file */ + +function __stubFnDoNotCall(): never { + throw new Error('this function is a stub and should never be called'); +} +/** Stub for appendFile */ +export const appendFile = __stubFnDoNotCall; +/** Stub for appendFileSync */ +export const appendFileSync = __stubFnDoNotCall; +/** Stub for access */ +export const access = __stubFnDoNotCall; +/** Stub for accessSync */ +export const accessSync = __stubFnDoNotCall; +/** Stub for chown */ +export const chown = __stubFnDoNotCall; +/** Stub for chownSync */ +export const chownSync = __stubFnDoNotCall; +/** Stub for chmod */ +export const chmod = __stubFnDoNotCall; +/** Stub for chmodSync */ +export const chmodSync = __stubFnDoNotCall; +/** Stub for close */ +export const close = __stubFnDoNotCall; +/** Stub for closeSync */ +export const closeSync = __stubFnDoNotCall; +/** Stub for copyFile */ +export const copyFile = __stubFnDoNotCall; +/** Stub for copyFileSync */ +export const copyFileSync = __stubFnDoNotCall; +/** Stub for cp */ +export const cp = __stubFnDoNotCall; +/** Stub for cpSync */ +export const cpSync = __stubFnDoNotCall; +/** Stub for createReadStream */ +export const createReadStream = __stubFnDoNotCall; +/** Stub for createWriteStream */ +export const createWriteStream = __stubFnDoNotCall; +/** Stub for exists */ +export const exists = __stubFnDoNotCall; +/** Stub for existsSync */ +export const existsSync = __stubFnDoNotCall; +/** Stub for fchown */ +export const fchown = __stubFnDoNotCall; +/** Stub for fchownSync */ +export const fchownSync = __stubFnDoNotCall; +/** Stub for fchmod */ +export const fchmod = __stubFnDoNotCall; +/** Stub for fchmodSync */ +export const fchmodSync = __stubFnDoNotCall; +/** Stub for fdatasync */ +export const fdatasync = __stubFnDoNotCall; +/** Stub for fdatasyncSync */ +export const fdatasyncSync = __stubFnDoNotCall; +/** Stub for fstat */ +export const fstat = __stubFnDoNotCall; +/** Stub for fstatSync */ +export const fstatSync = __stubFnDoNotCall; +/** Stub for fsync */ +export const fsync = __stubFnDoNotCall; +/** Stub for fsyncSync */ +export const fsyncSync = __stubFnDoNotCall; +/** Stub for ftruncate */ +export const ftruncate = __stubFnDoNotCall; +/** Stub for ftruncateSync */ +export const ftruncateSync = __stubFnDoNotCall; +/** Stub for futimes */ +export const futimes = __stubFnDoNotCall; +/** Stub for futimesSync */ +export const futimesSync = __stubFnDoNotCall; +/** Stub for lchown */ +export const lchown = __stubFnDoNotCall; +/** Stub for lchownSync */ +export const lchownSync = __stubFnDoNotCall; +/** Stub for lchmod */ +export const lchmod = undefined; +/** Stub for lchmodSync */ +export const lchmodSync = undefined; +/** Stub for link */ +export const link = __stubFnDoNotCall; +/** Stub for linkSync */ +export const linkSync = __stubFnDoNotCall; +/** Stub for lstat */ +export const lstat = __stubFnDoNotCall; +/** Stub for lstatSync */ +export const lstatSync = __stubFnDoNotCall; +/** Stub for lutimes */ +export const lutimes = __stubFnDoNotCall; +/** Stub for lutimesSync */ +export const lutimesSync = __stubFnDoNotCall; +/** Stub for mkdir */ +export const mkdir = __stubFnDoNotCall; +/** Stub for mkdirSync */ +export const mkdirSync = __stubFnDoNotCall; +/** Stub for mkdtemp */ +export const mkdtemp = __stubFnDoNotCall; +/** Stub for mkdtempSync */ +export const mkdtempSync = __stubFnDoNotCall; +/** Stub for open */ +export const open = __stubFnDoNotCall; +/** Stub for openSync */ +export const openSync = __stubFnDoNotCall; +/** Stub for opendir */ +export const opendir = __stubFnDoNotCall; +/** Stub for opendirSync */ +export const opendirSync = __stubFnDoNotCall; +/** Stub for readdir */ +export const readdir = __stubFnDoNotCall; +/** Stub for readdirSync */ +export const readdirSync = __stubFnDoNotCall; +/** Stub for read */ +export const read = __stubFnDoNotCall; +/** Stub for readSync */ +export const readSync = __stubFnDoNotCall; +/** Stub for readv */ +export const readv = __stubFnDoNotCall; +/** Stub for readvSync */ +export const readvSync = __stubFnDoNotCall; +/** Stub for readFile */ +export const readFile = __stubFnDoNotCall; +/** Stub for readFileSync */ +export const readFileSync = __stubFnDoNotCall; +/** Stub for readlink */ +export const readlink = __stubFnDoNotCall; +/** Stub for readlinkSync */ +export const readlinkSync = __stubFnDoNotCall; +/** Stub for realpath */ +export const realpath = __stubFnDoNotCall; +/** Stub for realpathSync */ +export const realpathSync = __stubFnDoNotCall; +/** Stub for rename */ +export const rename = __stubFnDoNotCall; +/** Stub for renameSync */ +export const renameSync = __stubFnDoNotCall; +/** Stub for rm */ +export const rm = __stubFnDoNotCall; +/** Stub for rmSync */ +export const rmSync = __stubFnDoNotCall; +/** Stub for rmdir */ +export const rmdir = __stubFnDoNotCall; +/** Stub for rmdirSync */ +export const rmdirSync = __stubFnDoNotCall; +/** Stub for stat */ +export const stat = __stubFnDoNotCall; +/** Stub for statSync */ +export const statSync = __stubFnDoNotCall; +/** Stub for symlink */ +export const symlink = __stubFnDoNotCall; +/** Stub for symlinkSync */ +export const symlinkSync = __stubFnDoNotCall; +/** Stub for truncate */ +export const truncate = __stubFnDoNotCall; +/** Stub for truncateSync */ +export const truncateSync = __stubFnDoNotCall; +/** Stub for unwatchFile */ +export const unwatchFile = __stubFnDoNotCall; +/** Stub for unlink */ +export const unlink = __stubFnDoNotCall; +/** Stub for unlinkSync */ +export const unlinkSync = __stubFnDoNotCall; +/** Stub for utimes */ +export const utimes = __stubFnDoNotCall; +/** Stub for utimesSync */ +export const utimesSync = __stubFnDoNotCall; +/** Stub for watch */ +export const watch = __stubFnDoNotCall; +/** Stub for watchFile */ +export const watchFile = __stubFnDoNotCall; +/** Stub for writeFile */ +export const writeFile = __stubFnDoNotCall; +/** Stub for writeFileSync */ +export const writeFileSync = __stubFnDoNotCall; +/** Stub for write */ +export const write = __stubFnDoNotCall; +/** Stub for writeSync */ +export const writeSync = __stubFnDoNotCall; +/** Stub for writev */ +export const writev = __stubFnDoNotCall; +/** Stub for writevSync */ +export const writevSync = __stubFnDoNotCall; +/** Stub for Dir */ +export const Dir = __stubFnDoNotCall; +/** Stub for Dirent */ +export const Dirent = __stubFnDoNotCall; +/** Stub for Stats */ +export const Stats = __stubFnDoNotCall; +/** Stub for ReadStream */ +export const ReadStream = __stubFnDoNotCall; +/** Stub for WriteStream */ +export const WriteStream = __stubFnDoNotCall; +/** Stub for FileReadStream */ +export const FileReadStream = __stubFnDoNotCall; +/** Stub for FileWriteStream */ +export const FileWriteStream = __stubFnDoNotCall; +/** Stub for _toUnixTimestamp */ +export const _toUnixTimestamp = __stubFnDoNotCall; +/** Stub for F_OK */ +export const F_OK = 0; +/** Stub for R_OK */ +export const R_OK = 4; +/** Stub for W_OK */ +export const W_OK = 2; +/** Stub for X_OK */ +export const X_OK = 1; +/** Stub for constants */ +export const constants = { + UV_FS_SYMLINK_DIR: 1, + UV_FS_SYMLINK_JUNCTION: 2, + O_RDONLY: 0, + O_WRONLY: 1, + O_RDWR: 2, + UV_DIRENT_UNKNOWN: 0, + UV_DIRENT_FILE: 1, + UV_DIRENT_DIR: 2, + UV_DIRENT_LINK: 3, + UV_DIRENT_FIFO: 4, + UV_DIRENT_SOCKET: 5, + UV_DIRENT_CHAR: 6, + UV_DIRENT_BLOCK: 7, + S_IFMT: 61440, + S_IFREG: 32768, + S_IFDIR: 16384, + S_IFCHR: 8192, + S_IFBLK: 24576, + S_IFIFO: 4096, + S_IFLNK: 40960, + S_IFSOCK: 49152, + O_CREAT: 64, + O_EXCL: 128, + UV_FS_O_FILEMAP: 0, + O_NOCTTY: 256, + O_TRUNC: 512, + O_APPEND: 1024, + O_DIRECTORY: 16384, + O_NOATIME: 262144, + O_NOFOLLOW: 32768, + O_SYNC: 1052672, + O_DSYNC: 4096, + O_DIRECT: 65536, + O_NONBLOCK: 2048, + S_IRWXU: 448, + S_IRUSR: 256, + S_IWUSR: 128, + S_IXUSR: 64, + S_IRWXG: 56, + S_IRGRP: 32, + S_IWGRP: 16, + S_IXGRP: 8, + S_IRWXO: 7, + S_IROTH: 4, + S_IWOTH: 2, + S_IXOTH: 1, + F_OK: 0, + R_OK: 4, + W_OK: 2, + X_OK: 1, + UV_FS_COPYFILE_EXCL: 1, + COPYFILE_EXCL: 1, + UV_FS_COPYFILE_FICLONE: 2, + COPYFILE_FICLONE: 2, + UV_FS_COPYFILE_FICLONE_FORCE: 4, + COPYFILE_FICLONE_FORCE: 4, +}; +/** Stub for promises */ +export const promises = { + access: __stubFnDoNotCall, + copyFile: __stubFnDoNotCall, + cp: __stubFnDoNotCall, + open: __stubFnDoNotCall, + opendir: __stubFnDoNotCall, + rename: __stubFnDoNotCall, + truncate: __stubFnDoNotCall, + rm: __stubFnDoNotCall, + rmdir: __stubFnDoNotCall, + mkdir: __stubFnDoNotCall, + readdir: __stubFnDoNotCall, + readlink: __stubFnDoNotCall, + symlink: __stubFnDoNotCall, + lstat: __stubFnDoNotCall, + stat: __stubFnDoNotCall, + link: __stubFnDoNotCall, + unlink: __stubFnDoNotCall, + chmod: __stubFnDoNotCall, + lchmod: __stubFnDoNotCall, + lchown: __stubFnDoNotCall, + chown: __stubFnDoNotCall, + utimes: __stubFnDoNotCall, + lutimes: __stubFnDoNotCall, + realpath: __stubFnDoNotCall, + mkdtemp: __stubFnDoNotCall, + writeFile: __stubFnDoNotCall, + appendFile: __stubFnDoNotCall, + readFile: __stubFnDoNotCall, + watch: __stubFnDoNotCall, + get constants() { + return constants; + }, +}; diff --git a/apps/rave-mark/frontend/test/setup.ts b/apps/rave-mark/frontend/test/setup.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/apps/rave-mark/frontend/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/apps/rave-mark/frontend/tsconfig.json b/apps/rave-mark/frontend/tsconfig.json index a48ecf9712..03d7c3b5cc 100644 --- a/apps/rave-mark/frontend/tsconfig.json +++ b/apps/rave-mark/frontend/tsconfig.json @@ -20,7 +20,6 @@ { "path": "../backend/tsconfig.build.json" }, { "path": "../../../libs/ballot-encoder/tsconfig.build.json" }, { "path": "../../../libs/basics/tsconfig.build.json" }, - { "path": "../../../libs/dev-dock/frontend/tsconfig.build.json" }, { "path": "../../../libs/eslint-plugin-vx/tsconfig.build.json" }, { "path": "../../../libs/fixtures/tsconfig.build.json" }, { "path": "../../../libs/grout/tsconfig.build.json" }, diff --git a/apps/rave-mark/frontend/vite.config.ts b/apps/rave-mark/frontend/vite.config.ts index 400e029172..3a25cf7c85 100644 --- a/apps/rave-mark/frontend/vite.config.ts +++ b/apps/rave-mark/frontend/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig((env) => { const processEnvDefines = [ ...Object.entries(rootDotenvValues), ...Object.entries(coreDotenvValues), + ['IS_INTEGRATION_TEST', process.env.IS_INTEGRATION_TEST || 'false'], ].reduce>( (acc, [key, value]) => ({ ...acc, @@ -25,9 +26,12 @@ export default defineConfig((env) => { {} ); + const basePort = Number(process.env.BASE_PORT) || 3000; + const port = Number(process.env.PORT) || basePort; + return { server: { - port: 3000, + port, }, build: { @@ -57,6 +61,7 @@ export default defineConfig((env) => { // The trailing slash is important, otherwise it will be resolved as a // built-in NodeJS module. { find: 'buffer', replacement: require.resolve('buffer/') }, + { find: 'fs', replacement: join(__dirname, './src/stubs/fs.ts') }, { find: 'path', replacement: require.resolve('path/') }, // Create aliases for all workspace packages, i.e. @@ -86,7 +91,7 @@ export default defineConfig((env) => { { name: 'development-proxy', configureServer: (app) => { - setupProxy(app.middlewares); + setupProxy(app.middlewares, basePort); }, }, ], diff --git a/apps/rave-mark/integration-testing/.eslintrc.json b/apps/rave-mark/integration-testing/.eslintrc.json deleted file mode 100644 index c13c1d5499..0000000000 --- a/apps/rave-mark/integration-testing/.eslintrc.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": ["plugin:vx/recommended"], - "overrides": [ - { - "files": ["cypress/**/*", "cypress.config.ts"], - "env": { - "cypress/globals": true - }, - "parserOptions": { - "project": "./tsconfig.json" - }, - "extends": ["plugin:cypress/recommended"], - "rules": { - // Cypress requires `cypress.config.ts` to use a default export. - "vx/gts-no-default-exports": "off", - // we do not need to document Cypress tests - "vx/gts-jsdoc": "off" - } - } - ] -} diff --git a/apps/rave-mark/integration-testing/.gitignore b/apps/rave-mark/integration-testing/.gitignore deleted file mode 100644 index 6981956ea6..0000000000 --- a/apps/rave-mark/integration-testing/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# cypress -/cypress/videos -/cypress/screenshots -/cypress/examples -/cypress/downloads -test-results.xml diff --git a/apps/rave-mark/integration-testing/cypress.config.ts b/apps/rave-mark/integration-testing/cypress.config.ts deleted file mode 100644 index 135e726c62..0000000000 --- a/apps/rave-mark/integration-testing/cypress.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'cypress'; - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:3000', - }, - - viewportWidth: 1920, - viewportHeight: 1080, -}); diff --git a/apps/rave-mark/integration-testing/cypress/e2e/example.cy.ts b/apps/rave-mark/integration-testing/cypress/e2e/example.cy.ts deleted file mode 100644 index dd736506bb..0000000000 --- a/apps/rave-mark/integration-testing/cypress/e2e/example.cy.ts +++ /dev/null @@ -1,6 +0,0 @@ -describe('template spec', () => { - it('passes', () => { - cy.visit('http://localhost:3000/'); - cy.contains('Hello RAVE'); - }); -}); diff --git a/apps/rave-mark/integration-testing/cypress/fixtures/example.json b/apps/rave-mark/integration-testing/cypress/fixtures/example.json deleted file mode 100644 index 02e4254378..0000000000 --- a/apps/rave-mark/integration-testing/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} diff --git a/apps/rave-mark/integration-testing/cypress/support/commands.ts b/apps/rave-mark/integration-testing/cypress/support/commands.ts deleted file mode 100644 index 119ab03f7c..0000000000 --- a/apps/rave-mark/integration-testing/cypress/support/commands.ts +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/apps/rave-mark/integration-testing/cypress/support/e2e.ts b/apps/rave-mark/integration-testing/cypress/support/e2e.ts deleted file mode 100644 index 5df9c0186c..0000000000 --- a/apps/rave-mark/integration-testing/cypress/support/e2e.ts +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/e2e.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands'; - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/apps/rave-mark/integration-testing/package.json b/apps/rave-mark/integration-testing/package.json deleted file mode 100644 index 9af679a02e..0000000000 --- a/apps/rave-mark/integration-testing/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@votingworks/rave-mark-integration-testing", - "version": "1.0.0", - "private": true, - "license": "GPL-3.0", - "author": "VotingWorks Eng ", - "scripts": { - "build": "pnpm --dir ../frontend run build", - "cypress:open": "DISPLAY=:0 cypress open --e2e --browser chromium", - "cypress:run": "is-ci cypress:run:ci cypress:run:local", - "cypress:run:ci": "cypress run --browser chrome --reporter junit", - "cypress:run:local": "DISPLAY=:0 cypress run --browser chromium --reporter junit", - "format": "prettier '**/*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)' --write", - "lint": "eslint .", - "lint:fix": "pnpm lint --fix", - "pre-commit": "lint-staged", - "start": "pnpm --dir ../frontend run start:prod", - "test": "start-server-and-test start http://localhost:3000 cypress:run", - "test:watch": "start-server-and-test start http://localhost:3000 cypress:open" - }, - "lint-staged": { - "*.+(css|graphql|json|less|md|mdx|sass|scss|yaml|yml)": [ - "prettier --write" - ], - "*.+(js|jsx|ts|tsx)": [ - "eslint --quiet --fix" - ], - "package.json": [ - "sort-package-json" - ] - }, - "dependencies": { - "cypress": "^12.13.0" - }, - "devDependencies": { - "@testing-library/cypress": "^9.0.0", - "@typescript-eslint/eslint-plugin": "5.37.0", - "@typescript-eslint/parser": "5.37.0", - "eslint": "8.23.1", - "eslint-config-airbnb-base": "^14.2.1", - "eslint-config-prettier": "^8.5.0", - "eslint-import-resolver-node": "^0.3.4", - "eslint-plugin-cypress": "^2.12.1", - "eslint-plugin-flowtype": "^5.2.0", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^26.1.5", - "eslint-plugin-jsx-a11y": "^6.6.1", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-vx": "workspace:*", - "is-ci-cli": "^2.1.2", - "start-server-and-test": "^1.12.5", - "typescript": "4.6.3" - } -} diff --git a/apps/rave-mark/integration-testing/tsconfig.json b/apps/rave-mark/integration-testing/tsconfig.json deleted file mode 100644 index 5640a0a22b..0000000000 --- a/apps/rave-mark/integration-testing/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../../tsconfig.shared.json", - "compilerOptions": { - "types": ["cypress"], - "isolatedModules": false, - "allowJs": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "lib": ["dom", "dom.iterable", "esnext"], - "module": "esnext", - "moduleResolution": "node", - "noEmit": true, - "noFallthroughCasesInSwitch": true, - "resolveJsonModule": true, - "skipLibCheck": true - }, - "include": ["**/*.ts"], - "exclude": [], - "references": [ - { "path": "../../../libs/eslint-plugin-vx/tsconfig.build.json" } - ] -} diff --git a/apps/rave-scan/Makefile b/apps/rave-scan/Makefile new file mode 100644 index 0000000000..f13b78e571 --- /dev/null +++ b/apps/rave-scan/Makefile @@ -0,0 +1,40 @@ +APP := rave-scan + +ALL: build + +clean: clean-frontend clean-backend + +clean-frontend: + @echo "🧹 Cleaning frontend…" + @cd frontend && dx clean + +clean-backend: + @echo "🧹 Cleaning backend…" + @cd backend && cargo clean + +build: build-frontend build-backend + +build-frontend: + @echo "🛠️ Building frontend…" + @cd frontend && dx build --release + +build-backend: + @echo "🛠️ Building backend…" + @cd backend && cargo build --release + +dist: build + @echo "📦 Packaging application…" + @rm -rf dist && mkdir dist + @cp -r frontend/dist dist/public + @cp ../../target/release/$(APP)-backend dist/$(APP) + @echo "- \e[34;4mdist/public\e[0m: frontend assets" + @echo "- \e[34;4mdist/$(APP)\e[0m: application binary" + +run: dist + @echo "🚀 Running application in production mode…" + @cd dist && ./$(APP) + +reset-db: + @echo "🗑️ Resetting database…" + @cd backend && cargo sqlx database reset --source db/migrations + @echo "✅ Database reset" \ No newline at end of file diff --git a/apps/rave-scan/backend/.env b/apps/rave-scan/backend/.env new file mode 100644 index 0000000000..d9d186fe31 --- /dev/null +++ b/apps/rave-scan/backend/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres:rave_scan diff --git a/apps/rave-scan/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json b/apps/rave-scan/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json new file mode 100644 index 0000000000..fe46f4f18d --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM elections\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0827f3926ea7cd3283dba9d4cb2029f9e35c697e4add1fbbcf34b3d6f8d265a7" +} diff --git a/apps/rave-scan/backend/.sqlx/query-0db11db12dc749b20f699f8dab01f5bb664b4a8441efc280fe6e2dd73ea6dcb5.json b/apps/rave-scan/backend/.sqlx/query-0db11db12dc749b20f699f8dab01f5bb664b4a8441efc280fe6e2dd73ea6dcb5.json new file mode 100644 index 0000000000..06f04f9ab8 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-0db11db12dc749b20f699f8dab01f5bb664b4a8441efc280fe6e2dd73ea6dcb5.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ClientId\",\n COALESCE(ARRAY_LENGTH(scanned_ballot_ids, 1), 0) as \"ballot_count!: _\",\n CAST(\n (\n SELECT COUNT(DISTINCT election_id)\n FROM scanned_ballots\n WHERE id IN (SELECT UNNEST(scanned_ballot_ids))\n ) AS int4\n ) AS \"election_count!: _\",\n CAST(\n (\n SELECT COUNT(*)\n FROM scanned_ballots\n WHERE id IN (SELECT UNNEST(scanned_ballot_ids))\n AND server_id IS NOT NULL\n ) AS int4\n ) AS \"synced_count!: _\",\n started_at,\n ended_at\n FROM batches\n ORDER BY started_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "ballot_count!: _", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "election_count!: _", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "synced_count!: _", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "started_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "ended_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + null, + null, + null, + false, + true + ] + }, + "hash": "0db11db12dc749b20f699f8dab01f5bb664b4a8441efc280fe6e2dd73ea6dcb5" +} diff --git a/apps/rave-scan/backend/.sqlx/query-168c524a19a63caf753903e070ab91b807b83a877f36ce1148e9dc84055567bb.json b/apps/rave-scan/backend/.sqlx/query-168c524a19a63caf753903e070ab91b807b83a877f36ce1148e9dc84055567bb.json new file mode 100644 index 0000000000..bd770011fc --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-168c524a19a63caf753903e070ab91b807b83a877f36ce1148e9dc84055567bb.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ClientId\",\n server_id as \"server_id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n definition as \"definition: String\",\n election_hash,\n created_at\n FROM elections\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "definition: String", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "election_hash", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "168c524a19a63caf753903e070ab91b807b83a877f36ce1148e9dc84055567bb" +} diff --git a/apps/rave-scan/backend/.sqlx/query-1e4cd90eb6b0ee7010004c2ea399552e44a2e72ac4082672453fe50d2ec9a959.json b/apps/rave-scan/backend/.sqlx/query-1e4cd90eb6b0ee7010004c2ea399552e44a2e72ac4082672453fe50d2ec9a959.json new file mode 100644 index 0000000000..87260ba21d --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-1e4cd90eb6b0ee7010004c2ea399552e44a2e72ac4082672453fe50d2ec9a959.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ClientId\",\n server_id as \"server_id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n definition as \"definition: String\",\n election_hash,\n created_at\n FROM elections\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "server_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "definition: String", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "election_hash", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "1e4cd90eb6b0ee7010004c2ea399552e44a2e72ac4082672453fe50d2ec9a959" +} diff --git a/apps/rave-scan/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json b/apps/rave-scan/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json new file mode 100644 index 0000000000..bee8b9c1f8 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859.json @@ -0,0 +1,19 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO elections (\n id,\n server_id,\n client_id,\n machine_id,\n election_hash,\n definition\n )\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (machine_id, client_id)\n DO UPDATE SET\n server_id = $2,\n election_hash = $5,\n definition = $6\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "251cd0c2c4af6a1a75f1960c48e5fca2b6c88cd47fe5053eb67f1193b8775859" +} diff --git a/apps/rave-scan/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json b/apps/rave-scan/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json new file mode 100644 index 0000000000..d7f252d0ee --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO scanned_ballots (\n id,\n server_id,\n client_id,\n machine_id,\n election_id,\n cast_vote_record,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (client_id, machine_id)\n DO UPDATE SET\n server_id = $2,\n election_id = $5,\n cast_vote_record = $6,\n created_at = $7\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Uuid", + "Bytea", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "2b3e7b73876dac34e6c42e96eac3a14a507f6927219b909fc023c35a3417aba6" +} diff --git a/apps/rave-scan/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json b/apps/rave-scan/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json new file mode 100644 index 0000000000..5cd9435a81 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registration_requests (\n id,\n server_id,\n client_id,\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ON CONFLICT (client_id, machine_id)\n DO UPDATE SET\n server_id = $2,\n common_access_card_id = $5,\n given_name = $6,\n family_name = $7,\n address_line_1 = $8,\n address_line_2 = $9,\n city = $10,\n state = $11,\n postal_code = $12,\n state_id = $13,\n created_at = $14\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "2ca95c570c82f261a2ce54d6354ced219a016ea6216c37fb17cd6082d6a68b4b" +} diff --git a/apps/rave-scan/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json b/apps/rave-scan/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json new file mode 100644 index 0000000000..a393f7a7a8 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM registrations\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "36d71ea1b7dff0bb9415f9563715423bb2b4166184dd1e917ce03665aa23b33b" +} diff --git a/apps/rave-scan/backend/.sqlx/query-389d4aa3005b51684781c3ce06a432308c5dd00dc14898452f7c0c0146e23595.json b/apps/rave-scan/backend/.sqlx/query-389d4aa3005b51684781c3ce06a432308c5dd00dc14898452f7c0c0146e23595.json new file mode 100644 index 0000000000..89e6cc297e --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-389d4aa3005b51684781c3ce06a432308c5dd00dc14898452f7c0c0146e23595.json @@ -0,0 +1,44 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n (SELECT client_id FROM elections WHERE id = election_id) as \"election_id!: ClientId\",\n cast_vote_record,\n created_at\n FROM scanned_ballots\n WHERE server_id IS NULL\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "election_id!: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + null, + false, + false + ] + }, + "hash": "389d4aa3005b51684781c3ce06a432308c5dd00dc14898452f7c0c0146e23595" +} diff --git a/apps/rave-scan/backend/.sqlx/query-39ea1da21f3ada5b21f0cf12bffbe3e2d3556da6ccc46afff1bfc76da84fe410.json b/apps/rave-scan/backend/.sqlx/query-39ea1da21f3ada5b21f0cf12bffbe3e2d3556da6ccc46afff1bfc76da84fe410.json new file mode 100644 index 0000000000..973820c8c7 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-39ea1da21f3ada5b21f0cf12bffbe3e2d3556da6ccc46afff1bfc76da84fe410.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n registration_request_id as \"registration_request_id: ClientId\",\n election_id as \"election_id: ClientId\",\n precinct_id,\n ballot_style_id\n FROM registrations\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "registration_request_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "election_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "precinct_id", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "ballot_style_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "39ea1da21f3ada5b21f0cf12bffbe3e2d3556da6ccc46afff1bfc76da84fe410" +} diff --git a/apps/rave-scan/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json b/apps/rave-scan/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json new file mode 100644 index 0000000000..a06d9b4b83 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO scanned_ballots (\n id,\n server_id,\n client_id,\n machine_id,\n election_id,\n cast_vote_record,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Uuid", + "Bytea", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "58571dbf3b269d22bce544f749178f43c6495ccfbc52509a8b6f28b3e286543c" +} diff --git a/apps/rave-scan/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json b/apps/rave-scan/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json new file mode 100644 index 0000000000..22a74de4fe --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM elections\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "60deb2575670cb139c6b6f8cb1ad54229b27e4235c24238959c688005a62638e" +} diff --git a/apps/rave-scan/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json b/apps/rave-scan/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json new file mode 100644 index 0000000000..0c15cf56ae --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM elections\n WHERE machine_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "69ec36f9f50b3ee06d88796646b70babdc7d08893cc5a2bbe5f25d866c61bd38" +} diff --git a/apps/rave-scan/backend/.sqlx/query-6fb997eb419320f00ff0e279e62075062e4412d1f2206ec3ecbcb79a3342c8da.json b/apps/rave-scan/backend/.sqlx/query-6fb997eb419320f00ff0e279e62075062e4412d1f2206ec3ecbcb79a3342c8da.json new file mode 100644 index 0000000000..ca104f3c79 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-6fb997eb419320f00ff0e279e62075062e4412d1f2206ec3ecbcb79a3342c8da.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n definition as \"definition: String\"\n FROM elections\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "definition: String", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "6fb997eb419320f00ff0e279e62075062e4412d1f2206ec3ecbcb79a3342c8da" +} diff --git a/apps/rave-scan/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json b/apps/rave-scan/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json new file mode 100644 index 0000000000..842fdd42bf --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM registrations\n WHERE machine_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "82970d2a487fcbe94c848827a76dde95fbe04f49e96a20227cce3b7d6b439e8a" +} diff --git a/apps/rave-scan/backend/.sqlx/query-869dcee67d4ee197b272b86b2ca9711116126764a1c8f600d00ceed977a20766.json b/apps/rave-scan/backend/.sqlx/query-869dcee67d4ee197b272b86b2ca9711116126764a1c8f600d00ceed977a20766.json new file mode 100644 index 0000000000..2704524e1d --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-869dcee67d4ee197b272b86b2ca9711116126764a1c8f600d00ceed977a20766.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO printed_ballots (\n id,\n server_id,\n client_id,\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n registration_id,\n cast_vote_record,\n cast_vote_record_signature,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (client_id, machine_id)\n DO UPDATE SET\n server_id = $2,\n cast_vote_record = $8,\n cast_vote_record_signature = $9,\n created_at = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea", + "Uuid", + "Bytea", + "Bytea", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "869dcee67d4ee197b272b86b2ca9711116126764a1c8f600d00ceed977a20766" +} diff --git a/apps/rave-scan/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json b/apps/rave-scan/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json new file mode 100644 index 0000000000..3715f74393 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM registration_requests\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "964959c23f452543f571ede3b5ee1153a3168603ff1df6548ac3249b1eb159c0" +} diff --git a/apps/rave-scan/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json b/apps/rave-scan/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json new file mode 100644 index 0000000000..c94203352f --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM printed_ballots\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "a091c9bd70d0c727999bc78d52fb263cd63057b11c0e90a822ac92c592fa255f" +} diff --git a/apps/rave-scan/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json b/apps/rave-scan/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json new file mode 100644 index 0000000000..497a2f28c3 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM scanned_ballots\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + true + ] + }, + "hash": "c19fea777676a831ee36a352cd814cba5470b4f364c123d6ea65ae1682b0eefe" +} diff --git a/apps/rave-scan/backend/.sqlx/query-ca8f21b8ec1238ad97fe75692ce5383d238c4a5a53768269230063419162668d.json b/apps/rave-scan/backend/.sqlx/query-ca8f21b8ec1238ad97fe75692ce5383d238c4a5a53768269230063419162668d.json new file mode 100644 index 0000000000..5321d06be4 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-ca8f21b8ec1238ad97fe75692ce5383d238c4a5a53768269230063419162668d.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE batches\n SET scanned_ballot_ids = ARRAY_APPEND(scanned_ballot_ids, $1)\n WHERE id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "ca8f21b8ec1238ad97fe75692ce5383d238c4a5a53768269230063419162668d" +} diff --git a/apps/rave-scan/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json b/apps/rave-scan/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json new file mode 100644 index 0000000000..f8b496c33e --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM scanned_ballots\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d95222a85acdaea5920302c8324b026a88493f8a891332420a6a25e4e2a98e99" +} diff --git a/apps/rave-scan/backend/.sqlx/query-dc16fad7a566eaf2ca2e567c723231f7ed617b75c3dcd617270f37318012d00d.json b/apps/rave-scan/backend/.sqlx/query-dc16fad7a566eaf2ca2e567c723231f7ed617b75c3dcd617270f37318012d00d.json new file mode 100644 index 0000000000..2c7f446012 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-dc16fad7a566eaf2ca2e567c723231f7ed617b75c3dcd617270f37318012d00d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE batches\n SET ended_at = NOW()\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "dc16fad7a566eaf2ca2e567c723231f7ed617b75c3dcd617270f37318012d00d" +} diff --git a/apps/rave-scan/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json b/apps/rave-scan/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json new file mode 100644 index 0000000000..85dd6ad814 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registrations (\n id,\n server_id,\n client_id,\n machine_id,\n common_access_card_id,\n registration_request_id,\n election_id,\n precinct_id,\n ballot_style_id,\n created_at\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (machine_id, client_id)\n DO UPDATE SET\n server_id = $2,\n common_access_card_id = $5,\n registration_request_id = $6,\n election_id = $7,\n precinct_id = $8,\n ballot_style_id = $9,\n created_at = $10\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "dcdcc10065214c4b59f5e7d16cf88a1adeeda2297e8b895c20d849b5e76e49e5" +} diff --git a/apps/rave-scan/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json b/apps/rave-scan/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json new file mode 100644 index 0000000000..20bda01bc2 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM registration_requests\n WHERE server_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e24b4b9014119314ee6f66fee7458ca99823e6507273b93a74d8c887f75b93cb" +} diff --git a/apps/rave-scan/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json b/apps/rave-scan/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json new file mode 100644 index 0000000000..09b0c05c15 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT server_id as \"server_id!: ServerId\"\n FROM registrations\n WHERE server_id IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "server_id!: ServerId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + }, + "hash": "e2798e63515574fb1aef1ae6bbbef7b690619520d45316712dc6cff9e5d74907" +} diff --git a/apps/rave-scan/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json b/apps/rave-scan/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json new file mode 100644 index 0000000000..89fc352bfc --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id as \"id: ClientId\"\n FROM printed_ballots\n WHERE machine_id = $1 AND client_id = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ClientId", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "e3bcfb0eff32f0f9cadd8d0f5910fcfe11a5232fb1a1b7740ee090600b6d0a7e" +} diff --git a/apps/rave-scan/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json b/apps/rave-scan/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json new file mode 100644 index 0000000000..d9cb368ec2 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e.json @@ -0,0 +1,80 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id\n FROM registration_requests\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "given_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "family_name", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "address_line_1", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "address_line_2", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "city", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "state", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "postal_code", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "state_id", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + }, + "hash": "ea7b97f4cfea652a964985ee7658974775a18f59a4d7ed9f247877cf43f6322e" +} diff --git a/apps/rave-scan/backend/.sqlx/query-f20f9b3ac334686762f3bddcac077b57a4d983779f8fbe4f5b67535e5179c554.json b/apps/rave-scan/backend/.sqlx/query-f20f9b3ac334686762f3bddcac077b57a4d983779f8fbe4f5b67535e5179c554.json new file mode 100644 index 0000000000..5d91aec1a1 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-f20f9b3ac334686762f3bddcac077b57a4d983779f8fbe4f5b67535e5179c554.json @@ -0,0 +1,56 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n registration_id as \"registration_id: ClientId\",\n cast_vote_record,\n cast_vote_record_signature\n FROM printed_ballots\n WHERE server_id IS NULL\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_certificate", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "registration_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 6, + "name": "cast_vote_record_signature", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f20f9b3ac334686762f3bddcac077b57a4d983779f8fbe4f5b67535e5179c554" +} diff --git a/apps/rave-scan/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json b/apps/rave-scan/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json new file mode 100644 index 0000000000..1d129dedb6 --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT created_at\n FROM elections\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef" +} diff --git a/apps/rave-scan/backend/.sqlx/query-fe73064275b12c3a341c87f926967f7e9e45b608e0dbb0d25af47a8bdc34dcae.json b/apps/rave-scan/backend/.sqlx/query-fe73064275b12c3a341c87f926967f7e9e45b608e0dbb0d25af47a8bdc34dcae.json new file mode 100644 index 0000000000..c24988fa3e --- /dev/null +++ b/apps/rave-scan/backend/.sqlx/query-fe73064275b12c3a341c87f926967f7e9e45b608e0dbb0d25af47a8bdc34dcae.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO batches (id, scanned_ballot_ids)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "UuidArray" + ] + }, + "nullable": [] + }, + "hash": "fe73064275b12c3a341c87f926967f7e9e45b608e0dbb0d25af47a8bdc34dcae" +} diff --git a/apps/rave-scan/backend/Cargo.toml b/apps/rave-scan/backend/Cargo.toml new file mode 100644 index 0000000000..bf148912d7 --- /dev/null +++ b/apps/rave-scan/backend/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "rave-scan-backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-stream = { workspace = true } +axum = { workspace = true } +ballot-encoder-rs = { workspace = true } +base64 = { workspace = true } +central-scanner = { workspace = true } +clap = { workspace = true } +color-eyre = { workspace = true } +dotenvy = { workspace = true } +env_logger = { workspace = true } +futures-core = { workspace = true } +image = { workspace = true } +rayon = { workspace = true } +reqwest = { workspace = true } +rqrr = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tower-http = { workspace = true, features = ["trace"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +types-rs = { workspace = true, features = ["backend"] } +uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/apps/rave-scan/backend/build.rs b/apps/rave-scan/backend/build.rs new file mode 100644 index 0000000000..7609593841 --- /dev/null +++ b/apps/rave-scan/backend/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} \ No newline at end of file diff --git a/apps/rave-scan/backend/db/migrations/20230705182146_init.sql b/apps/rave-scan/backend/db/migrations/20230705182146_init.sql new file mode 100644 index 0000000000..38b84df047 --- /dev/null +++ b/apps/rave-scan/backend/db/migrations/20230705182146_init.sql @@ -0,0 +1,90 @@ +create table elections ( + id uuid primary key, + -- generated on the server + server_id uuid not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_hash varchar(255) not null, + definition bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table registration_requests ( + id uuid primary key, + -- generated on the server + server_id uuid not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + given_name varchar(255) not null, + family_name varchar(255) not null, + address_line_1 varchar(255) not null, + address_line_2 varchar(255), + city varchar(255) not null, + state varchar(16) not null, + postal_code varchar(255) not null, + state_id varchar(255) not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table registrations ( + id uuid primary key, + -- generated on the server + server_id uuid not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + registration_request_id uuid not null references registration_requests(id) on update cascade on delete cascade, + election_id uuid not null references elections(id) on update cascade on delete cascade, + precinct_id varchar(255) not null, + ballot_style_id varchar(255) not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table printed_ballots ( + id uuid primary key, + -- generated on the server + server_id uuid not null, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + common_access_card_certificate bytea not null, + registration_id uuid not null references registrations(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + cast_vote_record_signature bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table scanned_ballots ( + id uuid primary key, + -- generated on the server, present only if the record has been synced + server_id uuid unique, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_id uuid not null references elections(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); diff --git a/apps/rave-scan/backend/db/migrations/20230911215714_add_batches.down.sql b/apps/rave-scan/backend/db/migrations/20230911215714_add_batches.down.sql new file mode 100644 index 0000000000..072126d4f2 --- /dev/null +++ b/apps/rave-scan/backend/db/migrations/20230911215714_add_batches.down.sql @@ -0,0 +1 @@ +drop table if exists batches; diff --git a/apps/rave-scan/backend/db/migrations/20230911215714_add_batches.sql b/apps/rave-scan/backend/db/migrations/20230911215714_add_batches.sql new file mode 100644 index 0000000000..1f18df87a2 --- /dev/null +++ b/apps/rave-scan/backend/db/migrations/20230911215714_add_batches.sql @@ -0,0 +1,7 @@ +create table batches ( + id uuid primary key, + scanned_ballot_ids uuid[] not null, + error_message text, + started_at timestamptz not null default current_timestamp, + ended_at timestamptz +); diff --git a/apps/rave-scan/backend/package.json b/apps/rave-scan/backend/package.json new file mode 100644 index 0000000000..01c2e79e28 --- /dev/null +++ b/apps/rave-scan/backend/package.json @@ -0,0 +1,21 @@ +{ + "name": "@votingworks/rave-scan-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "script/build", + "lint": "script/lint", + "start": "cargo watch -x run", + "test:ci": "script/ci", + "test:dev": "cargo test", + "test": "is-ci test:ci test:dev" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "devDependencies": { + "is-ci-cli": "2.2.0" + }, + "packageManager": "pnpm@8.1.0" +} diff --git a/apps/rave-scan/backend/script/build b/apps/rave-scan/backend/script/build new file mode 100755 index 0000000000..31f6fdd9cb --- /dev/null +++ b/apps/rave-scan/backend/script/build @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +SQLX_OFFLINE=true cargo build \ No newline at end of file diff --git a/apps/rave-scan/backend/script/ci b/apps/rave-scan/backend/script/ci new file mode 100755 index 0000000000..49d36b1895 --- /dev/null +++ b/apps/rave-scan/backend/script/ci @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +if [ $(id -u) -eq 0 ] && [ "${CI:-}" == true ]; then + service postgresql start + sudo -u postgres createuser --superuser $(whoami) || true + sudo -u postgres createdb $(cat .env | grep DATABASE_URL | cut -d '=' -f 2 - | cut -d ':' -f 2 -) || true +fi + +SQLX_OFFLINE=true cargo test \ No newline at end of file diff --git a/apps/rave-scan/backend/script/lint b/apps/rave-scan/backend/script/lint new file mode 100755 index 0000000000..ee15136feb --- /dev/null +++ b/apps/rave-scan/backend/script/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +SQLX_OFFLINE=true cargo clippy -- -D warnings \ No newline at end of file diff --git a/apps/rave-scan/backend/src/app.rs b/apps/rave-scan/backend/src/app.rs new file mode 100644 index 0000000000..916c0d5d3d --- /dev/null +++ b/apps/rave-scan/backend/src/app.rs @@ -0,0 +1,194 @@ +//! Application definition, including all HTTP route handlers. +//! +//! Route handlers are bundled via [`setup`] into an [`axum::Router`], which can then be run +//! using [`run`] at the configured port (see [`config`][`super::config`]). + +use std::convert::Infallible; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::channel; +use std::time::Duration; + +use async_stream::try_stream; +use axum::{ + extract::{DefaultBodyLimit, State}, + http::StatusCode, + response::{ + sse::{Event, KeepAlive, Sse}, + IntoResponse, + }, + routing::{get, post}, + Json, Router, +}; +use central_scanner::scan; +use futures_core::Stream; +use serde::Serialize; +use serde_json::json; +use sqlx::PgPool; +use time::OffsetDateTime; +use tokio::time::sleep; +use tower_http::services::{ServeDir, ServeFile}; +use tower_http::trace::TraceLayer; +use tracing::Level; +use types_rs::election::PartialElectionHash; +use types_rs::rave::ClientId; +use types_rs::scan::ScannedBallotStats; + +use crate::config::{Config, MAX_REQUEST_SIZE}; +use crate::db::{self, ScannedBallot}; +use crate::sheets::decode_page_from_image; + +type AppState = (Config, PgPool); + +/// Prepares the application with all the routes. Run the application with +/// `app::run(…)` once you have it. +pub(crate) async fn setup(pool: PgPool, config: Config) -> color_eyre::Result { + let _entered = tracing::span!(Level::DEBUG, "Setting up application").entered(); + + let dist_path = Path::new("../frontend/dist"); + let _ = std::fs::create_dir_all(dist_path); + + Ok(Router::new() + .fallback_service( + ServeDir::new(dist_path) + .append_index_html_on_directories(true) + .fallback(ServeFile::new(dist_path.join("index.html"))), + ) + .route("/api/status", get(get_status)) + .route("/api/status-stream", get(get_status_stream)) + .route("/api/scan", post(do_scan)) + .layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE)) + .layer(TraceLayer::new_for_http()) + .with_state((config, pool))) +} + +/// Runs an application built by `app::setup(…)`. +pub(crate) async fn run(app: Router, config: &Config) -> color_eyre::Result<()> { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), config.port); + tracing::info!("Server listening at http://{addr}/"); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await?; + Ok(()) +} + +#[derive(Debug, Serialize)] +pub(crate) struct ScannedCard { + cvr_data: Vec, + election_hash: PartialElectionHash, +} + +pub(crate) async fn get_status() -> impl IntoResponse { + StatusCode::OK +} + +pub(crate) async fn get_status_stream( + State((_, pool)): State, +) -> Sse>> { + let mut last_scanned_ballot_stats = ScannedBallotStats::default(); + + Sse::new(try_stream! { + loop { + let mut connection = pool.acquire().await.unwrap(); + match db::get_scanned_ballot_stats(&mut connection).await { + Ok(scanned_ballot_stats) => { + if scanned_ballot_stats != last_scanned_ballot_stats { + last_scanned_ballot_stats = scanned_ballot_stats.clone(); + yield Event::default().json_data(&json!({ + "status": "ok", + "stats": scanned_ballot_stats, + })).unwrap(); + } + } + Err(e) => { + yield Event::default().json_data(&json!({ + "status": "error", + "error": format!("{}", e), + })).unwrap(); + } + } + + sleep(Duration::from_millis(100)).await; + } + }) + .keep_alive(KeepAlive::default()) +} + +pub(crate) async fn do_scan(State((config, pool)): State) -> Json> { + let mut connection = pool.acquire().await.unwrap(); + let (tx, rx) = channel(); + + let batch_id = db::start_batch(&mut connection).await.unwrap(); + + let handle = std::thread::spawn(move || { + let session = scan(PathBuf::from("/tmp")).unwrap(); + for (side_a_path, side_b_path) in session { + tx.send((side_a_path, side_b_path)).expect("send() failed"); + } + }); + + let elections = db::get_elections(&mut connection, None).await.unwrap(); + + let mut cards = vec![]; + for (side_a_path, side_b_path) in rx { + let (side_a_result, side_b_result) = rayon::join( + move || decode_page_from_image(image::open(side_a_path).unwrap().to_luma8()), + move || decode_page_from_image(image::open(side_b_path).unwrap().to_luma8()), + ); + + match (side_a_result, side_b_result) { + (Err(side_a_err), Err(side_b_err)) => { + tracing::error!("Both sides failed: {side_a_err:?}, {side_b_err:?}"); + break; + } + (Ok(cvr_data), _) | (_, Ok(cvr_data)) => { + let (_, election_hash) = ballot_encoder_rs::decode_header(&cvr_data).unwrap(); + cards.push(ScannedCard { + election_hash, + cvr_data, + }) + } + } + } + + for card in cards.iter() { + if let Some(election) = elections.iter().find(|election| { + card.election_hash + .matches_election_hash(&election.election_hash) + }) { + let decoded_cvr = + ballot_encoder_rs::decode(&election.definition.election, card.cvr_data.as_slice()) + .unwrap(); + let cast_vote_record = serde_json::to_string(&decoded_cvr.cvr).unwrap(); + + let scanned_ballot_id = ClientId::new(); + let scanned_ballot = ScannedBallot { + id: scanned_ballot_id, + server_id: None, + client_id: scanned_ballot_id, + machine_id: config.machine_id.clone(), + election_id: election.id, + cast_vote_record: cast_vote_record.into_bytes(), + created_at: OffsetDateTime::now_utc(), + }; + + match db::add_scanned_ballot(&mut connection, scanned_ballot, batch_id).await { + Ok(_) => {} + Err(e) => { + tracing::error!("Failed to insert scanned ballot: {e}"); + } + } + } else { + tracing::error!( + "No election found for card with hash {}", + card.election_hash + ); + } + } + + handle.join().unwrap(); + + db::end_batch(&mut connection, batch_id).await.unwrap(); + + Json(cards) +} diff --git a/apps/rave-scan/backend/src/config.rs b/apps/rave-scan/backend/src/config.rs new file mode 100644 index 0000000000..754c5bc06f --- /dev/null +++ b/apps/rave-scan/backend/src/config.rs @@ -0,0 +1,38 @@ +//! Application configuration. + +use std::time::Duration; + +use clap::Parser; + +const TEN_MB: usize = 10 * 1024 * 1024; + +pub(crate) const MAX_REQUEST_SIZE: usize = TEN_MB; +pub(crate) const SYNC_INTERVAL: Duration = Duration::from_secs(5); + +#[derive(Debug, Clone, Parser)] +#[command(author, version, about)] +pub(crate) struct Config { + /// URL of the RAVE server, e.g. `https://rave.example.com/`. + #[arg(long, env = "RAVE_URL")] + pub(crate) rave_url: reqwest::Url, + + /// URL of the PostgreSQL database, e.g. `postgres://user:pass@host:port/dbname`. + #[arg(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + /// ID of this machine, e.g. `machine-1`. + #[arg(long, env = "VX_MACHINE_ID")] + pub(crate) machine_id: String, + + /// Port to listen on. + #[arg(long, env = "PORT")] + pub(crate) port: u16, + + /// Directory to serve static files from. + #[arg(long, env = "PUBLIC_DIR")] + pub(crate) public_dir: Option, + + /// Log level. + #[arg(long, env = "LOG_LEVEL", default_value = "info")] + pub(crate) log_level: tracing::Level, +} diff --git a/apps/rave-scan/backend/src/db.rs b/apps/rave-scan/backend/src/db.rs new file mode 100644 index 0000000000..28cc682108 --- /dev/null +++ b/apps/rave-scan/backend/src/db.rs @@ -0,0 +1,1013 @@ +//! Database access for the application. +//! +//! All direct use of [SQLx][`sqlx`] queries should be in this module. When +//! modifying this file, be sure to run `cargo sqlx prepare` in the application +//! root to regenerate the query metadata for offline builds. +//! +//! To enable `cargo sqlx prepare`, install it via `cargo install --locked +//! sqlx-cli`. + +use std::str::FromStr; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; +use tracing::Level; +use types_rs::election::{ElectionDefinition, ElectionHash}; +use types_rs::rave::{client, ClientId, ServerId}; +use types_rs::scan::{BatchStats, ScannedBallotStats}; + +use crate::config::Config; + +/// Sets up the database pool and runs any pending migrations, returning the +/// pool to be used by the app. +pub(crate) async fn setup(config: &Config) -> color_eyre::Result { + let _entered = tracing::span!(Level::DEBUG, "Setting up database").entered(); + let pool = PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(3)) + .connect(&config.database_url) + .await?; + sqlx::migrate!("db/migrations").run(&pool).await?; + Ok(pool) +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Election { + pub(crate) id: ClientId, + pub(crate) server_id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) definition: ElectionDefinition, + pub(crate) election_hash: ElectionHash, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ScannedBallot { + pub(crate) id: ClientId, + pub(crate) server_id: Option, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) election_id: ClientId, + pub(crate) cast_vote_record: Vec, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +pub(crate) async fn get_last_synced_election_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM elections + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_last_synced_registration_request_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM registration_requests + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_last_synced_registration_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM registrations + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_last_synced_printed_ballot_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM printed_ballots + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +#[allow(dead_code)] +pub(crate) async fn get_elections( + executor: &mut sqlx::PgConnection, + since_election_id: Option, +) -> Result, color_eyre::eyre::Error> { + let since_election = match since_election_id { + Some(id) => sqlx::query!( + r#" + SELECT created_at + FROM elections + WHERE id = $1 + "#, + id.as_uuid(), + ) + .fetch_optional(&mut *executor) + .await + .ok(), + None => None, + } + .flatten(); + + struct ElectionRecord { + id: ClientId, + server_id: ServerId, + client_id: ClientId, + machine_id: String, + definition: String, + election_hash: String, + created_at: sqlx::types::time::OffsetDateTime, + } + + let records = match since_election { + Some(election) => { + sqlx::query_as!( + ElectionRecord, + r#" + SELECT + id as "id: ClientId", + server_id as "server_id: ServerId", + client_id as "client_id: ClientId", + machine_id, + definition as "definition: String", + election_hash, + created_at + FROM elections + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + election.created_at + ) + .fetch_all(&mut *executor) + .await? + } + None => { + sqlx::query_as!( + ElectionRecord, + r#" + SELECT + id as "id: ClientId", + server_id as "server_id: ServerId", + client_id as "client_id: ClientId", + machine_id, + definition as "definition: String", + election_hash, + created_at + FROM elections + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await? + } + }; + + records + .into_iter() + .map(|record| { + Ok::(Election { + id: record.id, + server_id: record.server_id, + client_id: record.client_id, + machine_id: record.machine_id, + definition: record.definition.parse()?, + election_hash: ElectionHash::from_str(&record.election_hash)?, + created_at: record.created_at, + }) + }) + .collect::, _>>() +} + +pub(crate) async fn add_election_from_rave_server( + executor: &mut sqlx::PgConnection, + record: client::output::Election, +) -> color_eyre::Result { + sqlx::query!( + r#" + INSERT INTO elections ( + id, + server_id, + client_id, + machine_id, + election_hash, + definition + ) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (machine_id, client_id) + DO UPDATE SET + server_id = $2, + election_hash = $5, + definition = $6 + "#, + ClientId::new().as_uuid(), + record.server_id.as_uuid(), + record.client_id.as_uuid(), + record.machine_id, + record.definition.election_hash.as_str(), + record.definition.election_data, + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM elections + WHERE machine_id = $1 AND client_id = $2 + "#, + record.machine_id, + record.client_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_registration_from_rave_server( + executor: &mut sqlx::PgConnection, + registration: client::output::Registration, +) -> color_eyre::Result { + let registration_request_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registration_requests + WHERE server_id = $1 + "#, + registration.registration_request_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + let election_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM elections + WHERE server_id = $1 + "#, + registration.election_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + sqlx::query!( + r#" + INSERT INTO registrations ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (machine_id, client_id) + DO UPDATE SET + server_id = $2, + common_access_card_id = $5, + registration_request_id = $6, + election_id = $7, + precinct_id = $8, + ballot_style_id = $9, + created_at = $10 + "#, + ClientId::new().as_uuid(), + registration.server_id.as_uuid(), + registration.client_id.as_uuid(), + registration.machine_id, + registration.common_access_card_id, + registration_request_id.as_uuid(), + election_id.as_uuid(), + registration.precinct_id, + registration.ballot_style_id, + registration.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registrations + WHERE machine_id = $1 AND client_id = $2 + "#, + registration.machine_id, + registration.client_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_printed_ballot_from_rave_server( + executor: &mut sqlx::PgConnection, + printed_ballot: client::output::PrintedBallot, +) -> color_eyre::Result { + let registration_client_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registrations + WHERE server_id = $1 + "#, + printed_ballot.registration_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + sqlx::query!( + r#" + INSERT INTO printed_ballots ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ON CONFLICT (client_id, machine_id) + DO UPDATE SET + server_id = $2, + cast_vote_record = $8, + cast_vote_record_signature = $9, + created_at = $10 + "#, + ClientId::new().as_uuid(), + printed_ballot.server_id.as_uuid(), + printed_ballot.client_id.as_uuid(), + printed_ballot.machine_id, + printed_ballot.common_access_card_id, + printed_ballot.common_access_card_certificate, + registration_client_id.as_uuid(), + printed_ballot.cast_vote_record, + printed_ballot.cast_vote_record_signature, + printed_ballot.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM printed_ballots + WHERE machine_id = $1 AND client_id = $2 + "#, + printed_ballot.machine_id, + printed_ballot.client_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_scanned_ballot_from_rave_server( + executor: &mut sqlx::PgConnection, + scanned_ballot: client::output::ScannedBallot, +) -> color_eyre::Result { + let election_id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM elections + WHERE server_id = $1 + "#, + scanned_ballot.election_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + sqlx::query!( + r#" + INSERT INTO scanned_ballots ( + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (client_id, machine_id) + DO UPDATE SET + server_id = $2, + election_id = $5, + cast_vote_record = $6, + created_at = $7 + "#, + ClientId::new().as_uuid(), + scanned_ballot.server_id.as_uuid(), + scanned_ballot.client_id.as_uuid(), + scanned_ballot.machine_id, + election_id.as_uuid(), + scanned_ballot.cast_vote_record, + scanned_ballot.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM scanned_ballots + WHERE server_id = $1 + "#, + scanned_ballot.server_id.as_uuid(), + ) + .fetch_one(&mut *executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn add_or_update_registration_request_from_rave_server( + executor: &mut sqlx::PgConnection, + registration_request: client::output::RegistrationRequest, +) -> color_eyre::Result { + sqlx::query!( + r#" + INSERT INTO registration_requests ( + id, + server_id, + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ON CONFLICT (client_id, machine_id) + DO UPDATE SET + server_id = $2, + common_access_card_id = $5, + given_name = $6, + family_name = $7, + address_line_1 = $8, + address_line_2 = $9, + city = $10, + state = $11, + postal_code = $12, + state_id = $13, + created_at = $14 + "#, + ClientId::new().as_uuid(), + registration_request.server_id.as_uuid(), + registration_request.client_id.as_uuid(), + registration_request.machine_id, + registration_request.common_access_card_id, + registration_request.given_name, + registration_request.family_name, + registration_request.address_line_1, + registration_request.address_line_2, + registration_request.city, + registration_request.state, + registration_request.postal_code, + registration_request.state_id, + registration_request.created_at + ) + .execute(&mut *executor) + .await?; + + let id = sqlx::query!( + r#" + SELECT id as "id: ClientId" + FROM registration_requests + WHERE server_id = $1 + "#, + registration_request.server_id.as_uuid(), + ) + .fetch_one(executor) + .await? + .id; + + Ok(id) +} + +pub(crate) async fn get_registration_requests_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id + FROM registration_requests + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + Ok(records + .into_iter() + .map(|r| client::input::RegistrationRequest { + client_id: r.client_id, + machine_id: r.machine_id, + common_access_card_id: r.common_access_card_id, + given_name: r.given_name, + family_name: r.family_name, + address_line_1: r.address_line_1, + address_line_2: r.address_line_2, + city: r.city, + state: r.state, + postal_code: r.postal_code, + state_id: r.state_id, + }) + .collect()) +} + +pub(crate) async fn get_elections_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + definition as "definition: String" + FROM elections + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|e| { + Ok(client::input::Election { + client_id: e.client_id, + machine_id: e.machine_id, + definition: e.definition.parse()?, + }) + }) + .collect() +} + +pub(crate) async fn get_registrations_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + registration_request_id as "registration_request_id: ClientId", + election_id as "election_id: ClientId", + precinct_id, + ballot_style_id + FROM registrations + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + Ok(records + .into_iter() + .map(|r| client::input::Registration { + client_id: r.client_id, + machine_id: r.machine_id, + election_id: r.election_id, + registration_request_id: r.registration_request_id, + common_access_card_id: r.common_access_card_id, + precinct_id: r.precinct_id, + ballot_style_id: r.ballot_style_id, + }) + .collect()) +} + +pub(crate) async fn get_printed_ballots_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id as "registration_id: ClientId", + cast_vote_record, + cast_vote_record_signature + FROM printed_ballots + WHERE server_id IS NULL + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await?; + + records + .into_iter() + .map(|r| { + Ok(client::input::PrintedBallot { + client_id: r.client_id, + machine_id: r.machine_id, + common_access_card_id: r.common_access_card_id, + common_access_card_certificate: r.common_access_card_certificate, + registration_id: r.registration_id, + cast_vote_record: r.cast_vote_record, + cast_vote_record_signature: r.cast_vote_record_signature, + }) + }) + .collect() +} + +pub(crate) async fn get_scanned_ballots_to_sync_to_rave_server( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + let records = sqlx::query!( + r#" + SELECT + client_id as "client_id: ClientId", + machine_id, + (SELECT client_id FROM elections WHERE id = election_id) as "election_id!: ClientId", + cast_vote_record, + created_at + FROM scanned_ballots + WHERE server_id IS NULL + ORDER BY created_at DESC + "# + ) + .fetch_all(&mut *executor) + .await?; + + Ok(records + .into_iter() + .map(|r| client::input::ScannedBallot { + client_id: r.client_id, + machine_id: r.machine_id, + election_id: r.election_id, + cast_vote_record: r.cast_vote_record, + }) + .collect::>()) +} + +pub(crate) async fn start_batch( + executor: &mut sqlx::PgConnection, +) -> Result { + let batch_id = ClientId::new(); + + sqlx::query!( + r#" + INSERT INTO batches (id, scanned_ballot_ids) + VALUES ($1, $2) + "#, + batch_id.as_uuid(), + &vec![], + ) + .execute(executor) + .await?; + + Ok(batch_id) +} + +pub(crate) async fn end_batch( + executor: &mut sqlx::PgConnection, + batch_id: ClientId, +) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE batches + SET ended_at = NOW() + WHERE id = $1 + "#, + batch_id.as_uuid(), + ) + .execute(executor) + .await?; + + Ok(()) +} + +pub(crate) async fn add_scanned_ballot( + executor: &mut sqlx::PgConnection, + scanned_ballot: ScannedBallot, + batch_id: ClientId, +) -> Result<(), sqlx::Error> { + let ScannedBallot { + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at, + } = scanned_ballot; + sqlx::query!( + r#" + INSERT INTO scanned_ballots ( + id, + server_id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + "#, + id.as_uuid(), + server_id.map(|id| id.as_uuid()), + client_id.as_uuid(), + machine_id, + election_id.as_uuid(), + cast_vote_record, + created_at + ) + .execute(&mut *executor) + .await?; + + sqlx::query!( + r#" + UPDATE batches + SET scanned_ballot_ids = ARRAY_APPEND(scanned_ballot_ids, $1) + WHERE id = $2 + "#, + id.as_uuid(), + batch_id.as_uuid(), + ) + .execute(executor) + .await?; + + Ok(()) +} + +pub(crate) async fn get_last_synced_scanned_ballot_id( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + Ok(sqlx::query!( + r#" + SELECT server_id as "server_id!: ServerId" + FROM scanned_ballots + WHERE server_id IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *executor) + .await? + .map(|r| r.server_id)) +} + +pub(crate) async fn get_scanned_ballot_stats( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result { + let batches = sqlx::query_as!( + BatchStats, + r#" + SELECT + id as "id: ClientId", + COALESCE(ARRAY_LENGTH(scanned_ballot_ids, 1), 0) as "ballot_count!: _", + CAST( + ( + SELECT COUNT(DISTINCT election_id) + FROM scanned_ballots + WHERE id IN (SELECT UNNEST(scanned_ballot_ids)) + ) AS int4 + ) AS "election_count!: _", + CAST( + ( + SELECT COUNT(*) + FROM scanned_ballots + WHERE id IN (SELECT UNNEST(scanned_ballot_ids)) + AND server_id IS NOT NULL + ) AS int4 + ) AS "synced_count!: _", + started_at, + ended_at + FROM batches + ORDER BY started_at DESC + "# + ) + .fetch_all(executor) + .await?; + + Ok(ScannedBallotStats { batches }) +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + use time::OffsetDateTime; + use types_rs::cdf::cvr::Cvr; + + fn load_famous_names_election() -> ElectionDefinition { + let election_json = include_str!("../tests/fixtures/electionFamousNames2021.json"); + election_json.parse().unwrap() + } + + fn build_rave_server_registration_request() -> client::output::RegistrationRequest { + client::output::RegistrationRequest { + server_id: ServerId::new(), + client_id: ClientId::new(), + machine_id: "mark-terminal-001".to_owned(), + common_access_card_id: "0000000000".to_owned(), + given_name: "John".to_owned(), + family_name: "Doe".to_owned(), + address_line_1: "123 Main St".to_owned(), + address_line_2: None, + city: "Anytown".to_owned(), + state: "CA".to_owned(), + postal_code: "95959".to_owned(), + state_id: "CA-12345678".to_owned(), + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_election( + election_definition: ElectionDefinition, + ) -> client::output::Election { + client::output::Election { + server_id: ServerId::new(), + client_id: ClientId::new(), + machine_id: "mark-terminal-001".to_owned(), + election_hash: election_definition.election_hash.clone(), + definition: election_definition, + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_registration( + registration_request: &client::output::RegistrationRequest, + election: &client::output::Election, + election_definition: &ElectionDefinition, + ) -> client::output::Registration { + let ballot_style = &election_definition.election.ballot_styles[0]; + + client::output::Registration { + server_id: registration_request.server_id, + client_id: registration_request.client_id, + machine_id: registration_request.machine_id.clone(), + common_access_card_id: registration_request.common_access_card_id.clone(), + registration_request_id: registration_request.server_id, + election_id: election.server_id, + precinct_id: ballot_style.precincts[0].to_string(), + ballot_style_id: ballot_style.id.to_string(), + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_printed_ballot( + registration: &client::output::Registration, + cast_vote_record: Cvr, + ) -> client::output::PrintedBallot { + client::output::PrintedBallot { + server_id: registration.server_id, + client_id: registration.client_id, + machine_id: registration.machine_id.clone(), + common_access_card_id: registration.common_access_card_id.clone(), + common_access_card_certificate: vec![], + registration_id: registration.registration_request_id, + cast_vote_record: serde_json::to_vec(&cast_vote_record).unwrap(), + cast_vote_record_signature: vec![], + created_at: OffsetDateTime::now_utc(), + } + } + + fn build_rave_server_scanned_ballot( + election: &client::output::Election, + cast_vote_record: Cvr, + ) -> client::output::ScannedBallot { + client::output::ScannedBallot { + server_id: election.server_id, + client_id: election.client_id, + machine_id: election.machine_id.clone(), + cast_vote_record: serde_json::to_vec(&cast_vote_record).unwrap(), + election_id: election.server_id, + created_at: OffsetDateTime::now_utc(), + } + } + + #[sqlx::test(migrations = "db/migrations")] + async fn test_add_election_from_rave_server(pool: sqlx::PgPool) -> sqlx::Result<()> { + let mut db = pool.acquire().await?; + + let election_definition = load_famous_names_election(); + let record = build_rave_server_election(election_definition.clone()); + + let client_id = add_election_from_rave_server(&mut db, record.clone()) + .await + .unwrap(); + + // insert again, should be idempotent + let client_id2 = add_election_from_rave_server(&mut db, record.clone()) + .await + .unwrap(); + + assert_eq!(client_id, client_id2); + + Ok(()) + } + + #[sqlx::test(migrations = "db/migrations")] + async fn test_add_everything_to_database(pool: sqlx::PgPool) -> sqlx::Result<()> { + let mut db = pool.acquire().await?; + + let election_definition = load_famous_names_election(); + let registration_request = build_rave_server_registration_request(); + let election = build_rave_server_election(election_definition.clone()); + let registration = + build_rave_server_registration(®istration_request, &election, &election_definition); + let printed_ballot = build_rave_server_printed_ballot(®istration, Cvr::default()); + let scanned_ballot = build_rave_server_scanned_ballot(&election, Cvr::default()); + + add_election_from_rave_server(&mut db, election) + .await + .unwrap(); + + add_or_update_registration_request_from_rave_server(&mut db, registration_request) + .await + .unwrap(); + + add_or_update_registration_from_rave_server(&mut db, registration) + .await + .unwrap(); + + add_or_update_printed_ballot_from_rave_server(&mut db, printed_ballot) + .await + .unwrap(); + + add_or_update_scanned_ballot_from_rave_server(&mut db, scanned_ballot) + .await + .unwrap(); + + Ok(()) + } +} diff --git a/apps/rave-scan/backend/src/log.rs b/apps/rave-scan/backend/src/log.rs new file mode 100644 index 0000000000..e059f423d6 --- /dev/null +++ b/apps/rave-scan/backend/src/log.rs @@ -0,0 +1,35 @@ +//! Logging for RAVE Scan. +//! +//! RAVE Scan uses the `tracing` library for logging. After calling [`setup`], +//! you'll be able to call [`tracing::info!`], [`tracing::span!`], and others to +//! print log messages to `stdout` in a flexible and configurable way. +//! +//! You may use the `RUST_LOG` environment variable to configure logging at +//! runtime (see [`EnvFilter`][`tracing_subscriber::EnvFilter`]). + +use tracing_subscriber::{prelude::*, util::SubscriberInitExt}; + +use crate::config::Config; + +/// Sets up logging for the application. Call this early in the process +/// lifecycle to ensure logs are not silently ignored. +pub(crate) fn setup(config: &Config) -> color_eyre::Result<()> { + color_eyre::install()?; + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive( + format!( + "{}={}", + env!("CARGO_PKG_NAME").replace('-', "_"), + config.log_level + ) + .parse()?, + ) + .from_env_lossy(), + ) + .with(stdout_log) + .init(); + Ok(()) +} diff --git a/apps/rave-scan/backend/src/main.rs b/apps/rave-scan/backend/src/main.rs new file mode 100644 index 0000000000..63c781437a --- /dev/null +++ b/apps/rave-scan/backend/src/main.rs @@ -0,0 +1,61 @@ +//! RAVE Scan scans ballots and synchronizes them to the RAVE Server. + +#![warn( + clippy::all, + clippy::todo, + clippy::empty_enum, + clippy::enum_glob_use, + clippy::mem_forget, + clippy::unused_self, + clippy::filter_map_next, + clippy::needless_continue, + clippy::needless_borrow, + clippy::match_wildcard_for_single_variants, + clippy::if_let_mutex, + clippy::mismatched_target_os, + clippy::await_holding_lock, + clippy::match_on_vec_items, + clippy::imprecise_flops, + clippy::suboptimal_flops, + clippy::lossy_float_literal, + clippy::rest_pat_in_fully_bound_structs, + clippy::fn_params_excessive_bools, + clippy::exit, + clippy::inefficient_to_string, + clippy::linkedlist, + clippy::macro_use_imports, + clippy::option_option, + clippy::verbose_file_reads, + clippy::unnested_or_patterns, + clippy::str_to_string, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + missing_debug_implementations, + missing_docs +)] +#![deny(unreachable_pub, private_in_public)] +#![allow(elided_lifetimes_in_paths, clippy::type_complexity)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))] +#![cfg_attr(test, allow(clippy::float_cmp))] +#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))] + +use clap::Parser; + +mod app; +mod config; +mod db; +mod log; +mod sheets; +mod sync; + +#[tokio::main] +async fn main() -> color_eyre::Result<()> { + dotenvy::dotenv()?; + let config = config::Config::parse(); + log::setup(&config)?; + let pool = db::setup(&config).await?; + sync::sync_periodically(&pool, config.clone()).await; + app::run(app::setup(pool, config.clone()).await?, &config).await +} diff --git a/apps/rave-scan/backend/src/sheets.rs b/apps/rave-scan/backend/src/sheets.rs new file mode 100644 index 0000000000..7c3a47e11d --- /dev/null +++ b/apps/rave-scan/backend/src/sheets.rs @@ -0,0 +1,23 @@ +//! Utilities for extracting data from scanned ballot sheets. + +use base64::{engine::general_purpose::STANDARD, Engine}; +use color_eyre::eyre::eyre; +use rqrr::PreparedImage; + +pub(crate) fn decode_page_from_image(image: image::GrayImage) -> color_eyre::Result> { + let mut prepared_image = PreparedImage::prepare(image); + + prepared_image + .detect_grids() + .iter() + .flat_map(|g| g.decode()) + .next() + .map_or_else( + || Err(eyre!("No QR code found")), + |(_, content)| { + STANDARD + .decode(content.as_str()) + .map_err(|_| eyre!("Unable to decode QR code: {}", content)) + }, + ) +} diff --git a/apps/rave-scan/backend/src/sync.rs b/apps/rave-scan/backend/src/sync.rs new file mode 100644 index 0000000000..d0f3cdf981 --- /dev/null +++ b/apps/rave-scan/backend/src/sync.rs @@ -0,0 +1,218 @@ +//! RAVE Server synchronization utilities. + +use sqlx::PgPool; +use tokio::time::sleep; +use tracing::Level; +use types_rs::rave::{RaveServerSyncInput, RaveServerSyncOutput}; + +use crate::{ + config::{Config, SYNC_INTERVAL}, + db, +}; + +/// Spawns an async loop that synchronizes with the RAVE Server on a fixed +/// schedule. +pub(crate) async fn sync_periodically(pool: &PgPool, config: Config) { + tracing::debug!( + "Starting sync loop, syncing every {} seconds", + SYNC_INTERVAL.as_secs() + ); + let mut connection = pool + .acquire() + .await + .expect("failed to acquire database connection"); + + tokio::spawn(async move { + loop { + match sync(&mut connection, &config).await { + Ok(_) => { + tracing::info!("Successfully synced with RAVE Server"); + } + Err(e) => { + tracing::error!("Failed to sync with RAVE Server: {e}"); + } + } + + sleep(SYNC_INTERVAL).await; + } + }); +} + +pub(crate) async fn sync( + executor: &mut sqlx::PgConnection, + config: &Config, +) -> color_eyre::eyre::Result<()> { + let span = tracing::span!(Level::DEBUG, "Syncing with RAVE Server"); + let _enter = span.enter(); + + check_status(config.rave_url.join("/api/status")?).await?; + + let sync_input = RaveServerSyncInput { + last_synced_election_id: db::get_last_synced_election_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!("failed to get last synced election ID: {}", e)) + })?, + last_synced_registration_request_id: db::get_last_synced_registration_request_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get last synced registration request ID: {}", + e + )) + })?, + last_synced_registration_id: db::get_last_synced_registration_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!("failed to get last synced registration ID: {}", e)) + })?, + last_synced_scanned_ballot_id: db::get_last_synced_scanned_ballot_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get last synced scanned ballot ID: {}", + e + )) + })?, + last_synced_printed_ballot_id: db::get_last_synced_printed_ballot_id(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get last synced printed ballot ID: {}", + e + )) + })?, + elections: db::get_elections_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get elections to sync to RAVE Server: {}", + e + )) + })?, + registration_requests: db::get_registration_requests_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get registration requests to sync to RAVE Server: {}", + e + )) + })?, + registrations: db::get_registrations_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get registrations to sync to RAVE Server: {}", + e + )) + })?, + printed_ballots: db::get_printed_ballots_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get printed ballots to sync to RAVE Server: {}", + e + )) + })?, + scanned_ballots: db::get_scanned_ballots_to_sync_to_rave_server(executor) + .await + .map_err(|e| { + color_eyre::Report::msg(format!( + "failed to get scanned ballots to sync to RAVE Server: {}", + e + )) + })?, + }; + + let sync_endpoint = config + .rave_url + .join("/api/sync") + .expect("failed to construct sync URL"); + let sync_output = request(sync_endpoint, &sync_input).await?; + + let RaveServerSyncOutput { + elections, + registration_requests, + registrations, + printed_ballots, + scanned_ballots, + .. + } = sync_output.clone(); + + for election in elections.into_iter() { + let result = db::add_election_from_rave_server(executor, election).await; + + if let Err(e) = result { + tracing::error!("Failed to insert election: {}", e); + } + } + + for registration_request in registration_requests.into_iter() { + let result = + db::add_or_update_registration_request_from_rave_server(executor, registration_request) + .await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update registration request: {}", e); + } + } + + for registration in registrations.into_iter() { + let result = db::add_or_update_registration_from_rave_server(executor, registration).await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update registration: {}", e); + } + } + + for printed_ballot in printed_ballots.into_iter() { + let result = + db::add_or_update_printed_ballot_from_rave_server(executor, printed_ballot).await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update printed ballot: {}", e); + } + } + + for scanned_ballot in scanned_ballots.into_iter() { + let result = + db::add_or_update_scanned_ballot_from_rave_server(executor, scanned_ballot).await; + + if let Err(e) = result { + tracing::error!("Failed to insert or update scanned ballot: {}", e); + } + } + + Ok(()) +} + +pub(crate) async fn check_status(endpoint: reqwest::Url) -> color_eyre::eyre::Result<()> { + let client = reqwest::Client::new(); + client + .get(endpoint.clone()) + .send() + .await? + .error_for_status() + .map_err(|e| { + color_eyre::eyre::eyre!( + "RAVE Server responded with an error (status URL={}): {}", + endpoint, + e + ) + }) + .map(|_| ()) +} + +pub(crate) async fn request( + endpoint: reqwest::Url, + sync_input: &RaveServerSyncInput, +) -> color_eyre::eyre::Result { + let client = reqwest::Client::new(); + Ok(client + .post(endpoint) + .json(sync_input) + .send() + .await? + .json::() + .await?) +} diff --git a/apps/rave-scan/backend/tests/fixtures/electionFamousNames2021.json b/apps/rave-scan/backend/tests/fixtures/electionFamousNames2021.json new file mode 100755 index 0000000000..3e7a46c1fe --- /dev/null +++ b/apps/rave-scan/backend/tests/fixtures/electionFamousNames2021.json @@ -0,0 +1,324 @@ +{ + "title": "Lincoln Municipal General Election", + "state": "State of Hamilton", + "county": { + "id": "franklin", + "name": "Franklin County" + }, + "date": "2021-06-06T00:00:00-10:00", + "parties": [ + { + "id": "0", + "name": "Democrat", + "fullName": "Democratic Party", + "abbrev": "D" + }, + { + "id": "1", + "name": "Republican", + "fullName": "Republican Party", + "abbrev": "R" + }, + { + "id": "2", + "name": "Liberty", + "fullName": "Liberty Party", + "abbrev": "Li" + }, + { + "id": "3", + "name": "Green", + "fullName": "Green Party", + "abbrev": "G" + } + ], + "contests": [ + { + "id": "mayor", + "districtId": "district-1", + "type": "candidate", + "title": "Mayor", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "sherlock-holmes", + "name": "Sherlock Holmes", + "partyIds": ["0"] + }, + { + "id": "thomas-edison", + "name": "Thomas Edison", + "partyIds": ["1"] + } + ] + }, + { + "id": "controller", + "districtId": "district-1", + "type": "candidate", + "title": "Controller", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "winston-churchill", + "name": "Winston Churchill", + "partyIds": ["0"] + }, + { + "id": "oprah-winfrey", + "name": "Oprah Winfrey", + "partyIds": ["1"] + }, + { + "id": "louis-armstrong", + "name": "Louis Armstrong", + "partyIds": ["3"] + } + ] + }, + { + "id": "attorney", + "districtId": "district-1", + "type": "candidate", + "title": "Attorney", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "john-snow", + "name": "John Snow", + "partyIds": ["1"] + }, + { + "id": "mark-twain", + "name": "Mark Twain", + "partyIds": ["3"] + } + ] + }, + { + "id": "public-works-director", + "districtId": "district-1", + "type": "candidate", + "title": "Public Works Director", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "benjamin-franklin", + "name": "Benjamin Franklin", + "partyIds": ["0"] + }, + { + "id": "robert-downey-jr", + "name": "Robert Downey Jr.", + "partyIds": ["1"] + }, + { + "id": "bill-nye", + "name": "Bill Nye", + "partyIds": ["3"] + } + ] + }, + { + "id": "chief-of-police", + "districtId": "district-1", + "type": "candidate", + "title": "Chief of Police", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "natalie-portman", + "name": "Natalie Portman", + "partyIds": ["0"] + }, + { + "id": "frank-sinatra", + "name": "Frank Sinatra", + "partyIds": ["1"] + }, + { + "id": "andy-warhol", + "name": "Andy Warhol", + "partyIds": ["3"] + }, + { + "id": "alfred-hitchcock", + "name": "Alfred Hitchcock", + "partyIds": ["3"] + } + ] + }, + { + "id": "parks-and-recreation-director", + "districtId": "district-1", + "type": "candidate", + "title": "Parks and Recreation Director", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "charles-darwin", + "name": "Charles Darwin", + "partyIds": ["0"] + }, + { + "id": "stephen-hawking", + "name": "Stephen Hawking", + "partyIds": ["1"] + }, + { + "id": "johan-sebastian-bach", + "name": "Johann Sebastian Bach", + "partyIds": ["0"] + }, + { + "id": "alexander-graham-bell", + "name": "Alexander Graham Bell", + "partyIds": ["1"] + } + ] + }, + { + "id": "board-of-alderman", + "districtId": "district-1", + "type": "candidate", + "title": "Board of Alderman", + "seats": 4, + "allowWriteIns": true, + "candidates": [ + { + "id": "helen-keller", + "name": "Helen Keller", + "partyIds": ["0"] + }, + { + "id": "steve-jobs", + "name": "Steve Jobs", + "partyIds": ["1"] + }, + { + "id": "nikola-tesla", + "name": "Nikola Tesla", + "partyIds": ["0"] + }, + { + "id": "vincent-van-gogh", + "name": "Vincent Van Gogh", + "partyIds": ["1"] + }, + { + "id": "pablo-picasso", + "name": "Pablo Picasso", + "partyIds": ["1"] + }, + { + "id": "wolfgang-amadeus-mozart", + "name": "Wolfgang Amadeus Mozart", + "partyIds": ["2"] + } + ] + }, + { + "id": "city-council", + "districtId": "district-1", + "type": "candidate", + "title": "City Council", + "seats": 4, + "allowWriteIns": true, + "candidates": [ + { + "id": "marie-curie", + "name": "Marie Curie", + "partyIds": ["0"] + }, + { + "id": "indiana-jones", + "name": "Indiana Jones", + "partyIds": ["1"] + }, + { + "id": "mona-lisa", + "name": "Mona Lisa", + "partyIds": ["3"] + }, + { + "id": "jackie-chan", + "name": "Jackie Chan", + "partyIds": ["3"] + }, + { + "id": "tim-allen", + "name": "Tim Allen", + "partyIds": ["2"] + }, + { + "id": "mark-antony", + "name": "Mark Antony", + "partyIds": ["0"] + }, + { + "id": "harriet-tubman", + "name": "Harriet Tubman", + "partyIds": ["1"] + }, + { + "id": "martin-luther-king", + "name": "Dr. Martin Luther King Jr.", + "partyIds": ["0"] + }, + { + "id": "marilyn-monroe", + "name": "Marilyn Monroe", + "partyIds": ["1"] + } + ] + } + ], + "districts": [ + { + "id": "district-1", + "name": "City of Lincoln" + } + ], + "precincts": [ + { + "id": "23", + "name": "North Lincoln" + }, + { + "id": "22", + "name": "South Lincoln" + }, + { + "id": "21", + "name": "East Lincoln" + }, + { + "id": "20", + "name": "West Lincoln" + } + ], + "ballotStyles": [ + { + "id": "1", + "precincts": ["20", "21", "22", "23"], + "districts": ["district-1"] + } + ], + "sealUrl": "/seals/state-of-hamilton-official-seal.svg", + "adjudicationReasons": [ + "UninterpretableBallot", + "Overvote", + "Undervote", + "BlankBallot" + ], + "markThresholds": { + "definite": 0.12, + "marginal": 0.12 + } +} diff --git a/apps/rave-scan/frontend/.gitignore b/apps/rave-scan/frontend/.gitignore new file mode 100644 index 0000000000..ace9762350 --- /dev/null +++ b/apps/rave-scan/frontend/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/dist/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + +# Added by cargo + +/target diff --git a/apps/rave-scan/frontend/Cargo.toml b/apps/rave-scan/frontend/Cargo.toml new file mode 100644 index 0000000000..7c6555a611 --- /dev/null +++ b/apps/rave-scan/frontend/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "rave-scan-frontend" +version = "0.1.0" +authors = ["VotingWorks Eng "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +dioxus = "0.4" +dioxus-web = "0.4" + +log = "0.4.19" +dioxus-logger = "0.4.1" +console_error_panic_hook = "0.1.7" +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +types-rs = { workspace = true } +ui-rs = { workspace = true } +wasm-bindgen = "0.2.87" +web-sys = { version = "0.3.64", features = ["EventSource"] } diff --git a/apps/rave-scan/frontend/Dioxus.toml b/apps/rave-scan/frontend/Dioxus.toml new file mode 100644 index 0000000000..e029ffd788 --- /dev/null +++ b/apps/rave-scan/frontend/Dioxus.toml @@ -0,0 +1,45 @@ +[application] + +# App (Project) Name +name = "rave-scan-frontend" + +# Dioxus App Default Platform +# desktop, web, mobile, ssr +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (public) file folder +asset_dir = "public" + +[web.app] + +# HTML title tag content +title = "RAVE Scan" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "public"] + +# include `assets` in web platform +[web.resource] + +# CSS style file +style = ["/styles.css"] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] + +[[web.proxy]] +backend = "http://127.0.0.1:4001/api" diff --git a/apps/rave-scan/frontend/LICENSE b/apps/rave-scan/frontend/LICENSE new file mode 100644 index 0000000000..bcdd828e9c --- /dev/null +++ b/apps/rave-scan/frontend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Dioxus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/rave-scan/frontend/README.md b/apps/rave-scan/frontend/README.md new file mode 100644 index 0000000000..fa6e47524a --- /dev/null +++ b/apps/rave-scan/frontend/README.md @@ -0,0 +1,40 @@ +# dioxus-template + +> a template for starting a dioxus project to be used with [dioxus-cli](https://github.com/DioxusLabs/cli) + +## Usage + +#### use `dioxus-cli` init the template: + +``` +dioxus init hello-dioxus +``` + +or you can choose the template, for this tempalte: + +``` +dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template +``` + +#### Start a `dev-server` for the project: + +``` +cd ./hello-dioxus +dioxus serve +``` + +or package this project: + +``` +dioxus build --release +``` + +## Project Structure + +``` +.project +- public # save the assets you want include in your project. +- src # put your code +- - utils # save some public function +- - components # save some custom components +``` diff --git a/apps/rave-scan/frontend/input.css b/apps/rave-scan/frontend/input.css new file mode 100644 index 0000000000..bd6213e1df --- /dev/null +++ b/apps/rave-scan/frontend/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/apps/rave-scan/frontend/package.json b/apps/rave-scan/frontend/package.json new file mode 100644 index 0000000000..3404cbdf7b --- /dev/null +++ b/apps/rave-scan/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "@votingworks/rave-scan-frontend", + "version": "1.0.0", + "description": "RAVE Scan frontend", + "scripts": { + "build": "script/build", + "lint": "cargo fmt -- --check && cargo clippy -- -D warnings", + "start": "concurrently 'npm:start:*' 'npm:build:css:watch'", + "start:frontend": "dx serve --port 3000", + "start:backend": "pnpm --dir ../backend start", + "build:css:watch": "tailwindcss -i input.css -o ./public/styles.css --watch", + "test": "cargo test" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "dependencies": { + "tailwindcss": "^3.3.3" + }, + "devDependencies": { + "concurrently": "7.6.0" + }, + "packageManager": "pnpm@8.1.0" +} diff --git a/apps/rave-scan/frontend/public/styles.css b/apps/rave-scan/frontend/public/styles.css new file mode 100644 index 0000000000..2b8ea37fd1 --- /dev/null +++ b/apps/rave-scan/frontend/public/styles.css @@ -0,0 +1,725 @@ +/* +! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.m-2 { + margin: 0.5rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.flex { + display: flex; +} + +.table { + display: table; +} + +.hidden { + display: none; +} + +.h-screen { + height: 100vh; +} + +.w-screen { + width: 100vw; +} + +.flex-grow { + flex-grow: 1; +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.border { + border-width: 1px; +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-purple-500 { + --tw-bg-opacity: 1; + background-color: rgb(168 85 247 / var(--tw-bg-opacity)); +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.active\:bg-purple-700:active { + --tw-bg-opacity: 1; + background-color: rgb(126 34 206 / var(--tw-bg-opacity)); +} + +.disabled\:bg-purple-300:disabled { + --tw-bg-opacity: 1; + background-color: rgb(216 180 254 / var(--tw-bg-opacity)); +} + +@media (prefers-color-scheme: dark) { + .dark\:bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); + } + + .dark\:bg-slate-800 { + --tw-bg-opacity: 1; + background-color: rgb(30 41 59 / var(--tw-bg-opacity)); + } + + .dark\:text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); + } + + .dark\:text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); + } +} \ No newline at end of file diff --git a/apps/rave-scan/frontend/script/build b/apps/rave-scan/frontend/script/build new file mode 100755 index 0000000000..8e5208df7c --- /dev/null +++ b/apps/rave-scan/frontend/script/build @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if ! command dx --version &> /dev/null; then + cargo install dioxus-cli +fi + +rustup target add wasm32-unknown-unknown +dx build \ No newline at end of file diff --git a/apps/rave-scan/frontend/src/main.rs b/apps/rave-scan/frontend/src/main.rs new file mode 100644 index 0000000000..7a01d6a549 --- /dev/null +++ b/apps/rave-scan/frontend/src/main.rs @@ -0,0 +1,184 @@ +use dioxus::prelude::*; +use log::LevelFilter; +use serde::Deserialize; +use types_rs::scan::ScannedBallotStats; +use ui_rs::{Button, DateOrDateTimeCell, TableCell}; +use wasm_bindgen::prelude::*; +use web_sys::MessageEvent; + +fn main() { + // Init debug + dioxus_logger::init(LevelFilter::Info).expect("failed to init logger"); + console_error_panic_hook::set_once(); + + log::info!("starting app"); + dioxus_web::launch(App); +} + +fn get_root_url() -> reqwest::Url { + let loc = web_sys::window().unwrap().location(); + reqwest::Url::parse(loc.origin().unwrap().as_str()).unwrap() +} + +fn get_url(path: &str) -> reqwest::Url { + get_root_url().join(path).unwrap() +} + +fn use_scanned_ballot_stats(cx: Scope) -> &UseState> { + let ballot_stats = use_state(cx, || None); + + use_coroutine(cx, { + to_owned![ballot_stats]; + |_rx: UnboundedReceiver| async move { + #[derive(Deserialize)] + struct StatusStreamEvent { + stats: ScannedBallotStats, + } + let eventsource = web_sys::EventSource::new("/api/status-stream").unwrap(); + + let callback = Closure::wrap(Box::new(move |event: MessageEvent| { + if let Some(data) = event.data().as_string() { + match serde_json::from_str::(data.as_str()) { + Ok(event) => { + ballot_stats.set(Some(event.stats)); + } + Err(err) => { + log::error!("error deserializing status event: {:?}", err); + } + } + } + }) as Box); + + eventsource.set_onmessage(Some(callback.as_ref().unchecked_ref())); + callback.forget(); + } + }); + + ballot_stats +} + +#[allow(non_snake_case)] +fn App(cx: Scope) -> Element { + let is_scanning = use_state(cx, || false); + let ballot_stats = use_scanned_ballot_stats(cx); + + let scan_ballots = move |_| { + log::info!("scan ballots"); + is_scanning.set(true); + + cx.spawn({ + to_owned![is_scanning]; + async move { + let client = reqwest::Client::new(); + let result = client.post(get_url("/api/scan")).send().await; + is_scanning.set(false); + + match result { + Ok(response) => { + log::info!("response: {:?}", response); + + if !response.status().is_success() { + log::error!("error"); + return; + } + + log::info!("success"); + } + Err(err) => { + log::error!("error: {:?}", err); + } + } + } + }); + }; + + let is_scanning = *is_scanning.get(); + let ballot_stats = ballot_stats.get(); + + render! { + div { + class: "h-screen w-screen dark:bg-slate-800", + div { + class: "flex-col", + div { + class: "flex flex-row items-center mb-2 pl-2 bg-gray-200 dark:bg-gray-700", + div { + class: "text-3xl flex-grow font-bold text-gray-700 dark:text-gray-200", + "RAVE Scan" + } + div { + class: "m-2", + Button { + onclick: scan_ballots, + disabled: is_scanning, + if is_scanning { + "Scanning…" + } else { + "Scan Ballots" + }, + } + } + } + div { + class: "text-xl text-center text-gray-400 dark:text-gray-300 px-3", + if let Some(ballot_stats) = ballot_stats { + rsx! { + h3 { + class: "text-left", + "Batches" + } + table { + class: "text-sm", + thead { + tr { + th { "ID" } + th { "Status" } + th { "Started At" } + th { "Ended At" } + } + } + tbody { + for batch in ballot_stats.batches.clone().into_iter() { + rsx! { + tr { + TableCell { batch.id.to_string() } + TableCell { + match (batch.ballot_count, batch.election_count, batch.synced_count) { + (0, _, _) => "No ballots".to_owned(), + (1, _, 0) => "1 ballot (pending sync)".to_owned(), + (1, _, 1) => "1 ballot (synced)".to_owned(), + (b, 1, s) if b == s => format!("{b} ballots, 1 election (synced)"), + (b, 1, s) => format!("{b} ballots, 1 election ({s} synced)"), + (b, e, s) if b == s => format!("{b} ballots, {e} elections (synced)"), + (b, e, s) => format!("{b} ballots, {e} elections ({s} synced)"), + } + } + DateOrDateTimeCell { + date_or_datetime: batch.started_at, + } + match batch.ended_at { + Some(ended_at) => { + rsx! { + DateOrDateTimeCell { date_or_datetime: ended_at } + } + } + None => { + rsx! { + TableCell { + "Scanning…" + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/apps/rave-scan/frontend/tailwind.config.js b/apps/rave-scan/frontend/tailwind.config.js new file mode 100644 index 0000000000..e584227552 --- /dev/null +++ b/apps/rave-scan/frontend/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: 'all', + content: [ + './src/**/*.{rs,html,css}', + './dist/**/*.html', + '../../../libs/ui-rs/src/**/*.{rs,html,css}', + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/libs/backend/src/cast_vote_records/index.ts b/libs/backend/src/cast_vote_records/index.ts index 597e2ab254..9fbb1e9d18 100644 --- a/libs/backend/src/cast_vote_records/index.ts +++ b/libs/backend/src/cast_vote_records/index.ts @@ -1,5 +1,6 @@ /* istanbul ignore file */ export { + buildCastVoteRecord, buildCVRContestsFromVotes, buildCvrImageData, combineImageAndLayoutHashes, diff --git a/libs/ballot-encoder-rs/Cargo.toml b/libs/ballot-encoder-rs/Cargo.toml new file mode 100644 index 0000000000..1e2cfb0f1e --- /dev/null +++ b/libs/ballot-encoder-rs/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ballot-encoder-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitstream-io = { workspace = true } +color-eyre = { workspace = true } +hex = { workspace = true } +nanoid = { workspace = true } +types-rs = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/libs/ballot-encoder-rs/package.json b/libs/ballot-encoder-rs/package.json new file mode 100644 index 0000000000..69b6988f49 --- /dev/null +++ b/libs/ballot-encoder-rs/package.json @@ -0,0 +1,16 @@ +{ + "name": "ballot-encoder-rs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "cargo build", + "lint": "cargo clippy -- -D warnings", + "start": "cargo watch -x run", + "test": "cargo test" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "packageManager": "pnpm@8.1.0" +} diff --git a/libs/ballot-encoder-rs/src/consts.rs b/libs/ballot-encoder-rs/src/consts.rs new file mode 100644 index 0000000000..89d9c6b5b8 --- /dev/null +++ b/libs/ballot-encoder-rs/src/consts.rs @@ -0,0 +1,6 @@ +pub(crate) const ENCODING_VERSION: u8 = 2; +pub(crate) const ELECTION_HASH_HEX_LENGTH: usize = 20; +pub(crate) const ELECTION_HASH_BYTE_LENGTH: usize = ELECTION_HASH_HEX_LENGTH / 2; +pub(crate) const MAXIMUM_WRITE_IN_NAME_LENGTH: usize = 40; +pub(crate) const BITS_PER_WRITE_IN_CHAR: u32 = 5; +pub(crate) const WRITE_IN_CHARS: &str = r#"ABCDEFGHIJKLMNOPQRSTUVWXYZ '"-.,"#; diff --git a/libs/ballot-encoder-rs/src/decode.rs b/libs/ballot-encoder-rs/src/decode.rs new file mode 100644 index 0000000000..7b1dfd44c9 --- /dev/null +++ b/libs/ballot-encoder-rs/src/decode.rs @@ -0,0 +1,385 @@ +use crate::{ + consts::{ + BITS_PER_WRITE_IN_CHAR, ELECTION_HASH_BYTE_LENGTH, ENCODING_VERSION, + MAXIMUM_WRITE_IN_NAME_LENGTH, WRITE_IN_CHARS, + }, + types::{BallotConfig, EncodableCvr, TestMode}, + util::sizeof, +}; +use bitstream_io::{BigEndian, BitRead, BitReader, Endianness}; +use std::io::Read; +use types_rs::{ + cdf::cvr::{ + CVRContest, CVRContestSelection, CVRSnapshot, CVRWriteIn, Cvr, IndicationStatus, + SelectionPosition, VxBallotType, + }, + election::{BallotStyleId, Contest, Election, PartialElectionHash, PrecinctId}, +}; + +/// Decodes a CVR from data encoded in a BMD ballot card. +pub fn decode(election: &Election, data: &[u8]) -> std::io::Result { + let mut reader = bitstream_io::BitReader::endian(data, BigEndian); + let decoded = decode_from(election, &mut reader)?; + + while !reader.byte_aligned() { + if reader.read_bit()? { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "unexpected non-zero padding bit", + )); + } + } + + let mut remainder = vec![]; + let read_length = reader.into_reader().read_to_end(&mut remainder)?; + + if read_length > 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "unexpected trailing data", + )); + } + + Ok(decoded) +} + +/// Decodes the encoded header from data encoded in a BMD ballot card. This +/// should be used to validate the version & election hash before decoding the +/// rest of the CVR. +pub fn decode_header(data: &[u8]) -> std::io::Result<(u8, PartialElectionHash)> { + let mut reader = bitstream_io::BitReader::endian(data, BigEndian); + let version = decode_prelude_from(&mut reader)?; + let election_hash = decode_partial_election_hash_from(&mut reader)?; + + Ok((version, election_hash)) +} + +/// Decodes a CVR by reading data encoded in a BMD ballot card. +pub fn decode_from( + election: &Election, + reader: &mut BitReader<&[u8], E>, +) -> std::io::Result { + let version = decode_prelude_from(reader)?; + + if version != ENCODING_VERSION { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("unsupported version: {}", version), + )); + } + + let partial_election_hash = decode_partial_election_hash_from(reader)?; + let (precinct_count, ballot_style_count, contest_count) = decode_check_data_from(reader)?; + + if precinct_count as usize != election.precincts.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "precinct count mismatch: expected {}, but got {}", + election.precincts.len(), + precinct_count + ), + )); + } + + if ballot_style_count as usize != election.ballot_styles.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "ballot style count mismatch: expected {}, but got {}", + election.ballot_styles.len(), + ballot_style_count + ), + )); + } + + if contest_count as usize != election.contests.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "contest count mismatch: expected {}, but got {}", + election.contests.len(), + contest_count + ), + )); + } + + let ballot_config = decode_ballot_config_from(election, reader)?; + let ballot_style = election + .ballot_styles + .get(ballot_config.ballot_style_index as usize) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "cannot find ballot_style with index={}", + ballot_config.ballot_style_index + ), + ) + })?; + let precinct = election + .precincts + .get(ballot_config.precinct_index as usize) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "cannot find precinct with index={}", + ballot_config.precinct_index + ), + ) + })?; + let contests = election + .get_contests(ballot_style.id.clone()) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "cannot find contests for ballot_style_id={}", + ballot_style.id + ), + ) + })?; + let ballot_votes = decode_ballot_votes_from(contests, &ballot_style.id, &precinct.id, reader)?; + + Ok(EncodableCvr::new( + partial_election_hash, + ballot_votes, + ballot_config.ballot_mode, + )) +} + +pub fn decode_prelude_from(reader: &mut BitReader<&[u8], E>) -> std::io::Result { + let mut tag = [0u8; 2]; + reader.read_bytes(&mut tag)?; + if &tag != b"VX" { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid tag: {:?}", tag), + )); + } + + let version = reader.read(8)?; + + Ok(version) +} + +pub fn decode_partial_election_hash_from( + reader: &mut BitReader<&[u8], E>, +) -> std::io::Result { + let mut election_hash_bytes = [0u8; ELECTION_HASH_BYTE_LENGTH]; + reader.read_bytes(&mut election_hash_bytes)?; + + election_hash_bytes.try_into().map_err(|err| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid election hash: {}", err), + ) + }) +} + +pub fn decode_check_data_from( + reader: &mut BitReader<&[u8], E>, +) -> std::io::Result<(u8, u8, u8)> { + let precinct_count: u8 = reader.read(8)?; + let ballot_style_count: u8 = reader.read(8)?; + let contest_count: u8 = reader.read(8)?; + + Ok((precinct_count, ballot_style_count, contest_count)) +} + +pub fn decode_ballot_config_from( + election: &Election, + reader: &mut BitReader<&[u8], E>, +) -> std::io::Result { + let precinct_index = reader.read::(sizeof(election.precincts.len() - 1))?; + let ballot_style_index = reader.read::(sizeof(election.ballot_styles.len() - 1))?; + let ballot_mode = if reader.read_bit()? { + TestMode::Test + } else { + TestMode::Live + }; + let ballot_type: VxBallotType = reader + .read::(sizeof(VxBallotType::max() as usize))? + .into(); + let ballot_id_present = reader.read_bit()?; + let ballot_id = if ballot_id_present { + let ballot_id_length = reader.read::(8)?; + let mut ballot_id = vec![0u8; ballot_id_length as usize]; + reader.read_bytes(&mut ballot_id)?; + Some(String::from_utf8(ballot_id).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "ballot_id is not a valid UTF-8 string", + ) + })?) + } else { + None + }; + + Ok(BallotConfig { + precinct_index, + ballot_style_index, + ballot_type, + ballot_mode, + ballot_id, + }) +} + +pub fn decode_ballot_votes_from( + contests: Vec<&Contest>, + ballot_style_id: &BallotStyleId, + precinct_id: &PrecinctId, + reader: &mut BitReader<&[u8], E>, +) -> std::io::Result { + let mut contests_with_votes = vec![]; + + for contest in contests.iter() { + if reader.read_bit()? { + contests_with_votes.push(contest); + } + } + + let mut cvr_contests = vec![]; + + for contest in contests.iter() { + let contest_has_votes = contests_with_votes.contains(&contest); + + let cvr_contest_selections = match contest { + Contest::YesNo(_) => { + vec![ + CVRContestSelection { + contest_selection_id: Some("yes".to_string()), + option_position: Some(0), + selection_position: vec![SelectionPosition { + has_indication: if contest_has_votes && reader.read_bit()? { + IndicationStatus::Yes + } else { + IndicationStatus::No + }, + ..Default::default() + }], + ..Default::default() + }, + CVRContestSelection { + contest_selection_id: Some("no".to_string()), + option_position: Some(1), + selection_position: vec![SelectionPosition { + has_indication: if contest_has_votes && reader.read_bit()? { + IndicationStatus::Yes + } else { + IndicationStatus::No + }, + ..Default::default() + }], + ..Default::default() + }, + ] + } + Contest::Candidate(candidate_contest) => { + let mut cvr_contest_selections = vec![]; + let mut selected_candidate_count = 0usize; + + for (index, candidate) in candidate_contest.candidates.iter().enumerate() { + let has_vote = contest_has_votes && reader.read_bit()?; + if has_vote { + selected_candidate_count += 1; + } + cvr_contest_selections.push(CVRContestSelection { + contest_selection_id: Some(candidate.id.to_string()), + option_position: Some(index as i64), + selection_position: vec![SelectionPosition { + has_indication: if has_vote { + IndicationStatus::Yes + } else { + IndicationStatus::No + }, + ..Default::default() + }], + ..Default::default() + }); + } + + if contest_has_votes && candidate_contest.allow_write_ins { + let maximum_possible_write_ins = (candidate_contest.seats as usize + - selected_candidate_count) + .clamp(0, candidate_contest.candidates.len()); + + if maximum_possible_write_ins > 0 { + let write_in_count = + reader.read::(sizeof(maximum_possible_write_ins))?; + + for _ in 0..write_in_count { + let write_in_name = decode_write_in_name_from(reader)?; + cvr_contest_selections.push(CVRContestSelection { + contest_selection_id: None, + option_position: None, + selection_position: vec![SelectionPosition { + has_indication: IndicationStatus::Yes, + cvr_write_in: Some(CVRWriteIn { + text: Some(write_in_name), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }); + } + } + } + + cvr_contest_selections + } + }; + cvr_contests.push(CVRContest { + contest_id: contest.id().to_string(), + cvr_contest_selection: Some(cvr_contest_selections), + ..Default::default() + }); + } + + let cvr_snapshot = CVRSnapshot { + cvr_contest: Some(cvr_contests), + ..Default::default() + }; + + let cvr = Cvr { + ballot_style_id: Some(ballot_style_id.to_string()), + ballot_style_unit_id: Some(precinct_id.to_string()), + current_snapshot_id: cvr_snapshot.id.clone(), + cvr_snapshot: vec![cvr_snapshot], + ..Default::default() + }; + + Ok(cvr) +} + +pub fn decode_write_in_name_from( + reader: &mut BitReader<&[u8], E>, +) -> std::io::Result { + let write_in_name_length = reader.read::(sizeof(MAXIMUM_WRITE_IN_NAME_LENGTH))?; + + if write_in_name_length > MAXIMUM_WRITE_IN_NAME_LENGTH as u32 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("write-in name length is too long: {}", write_in_name_length), + )); + } + + let mut write_in_name = String::with_capacity(write_in_name_length as usize); + + for _ in 0..write_in_name_length { + let index = reader.read::(BITS_PER_WRITE_IN_CHAR)?; + let char = WRITE_IN_CHARS + .get(index as usize..=index as usize) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid write-in name char index: {}", index), + ) + })?; + write_in_name.push_str(char); + } + + Ok(write_in_name) +} diff --git a/libs/ballot-encoder-rs/src/encode.rs b/libs/ballot-encoder-rs/src/encode.rs new file mode 100644 index 0000000000..0ccfd75e48 --- /dev/null +++ b/libs/ballot-encoder-rs/src/encode.rs @@ -0,0 +1,385 @@ +use crate::{ + consts::{ + BITS_PER_WRITE_IN_CHAR, ENCODING_VERSION, MAXIMUM_WRITE_IN_NAME_LENGTH, WRITE_IN_CHARS, + }, + types::{EncodableCvr, TestMode}, + util::sizeof, +}; +use bitstream_io::{BigEndian, BitWrite, BitWriter, Endianness}; +use types_rs::{ + cdf::cvr::{CVRContestSelection, Cvr, IndicationStatus, VxBallotType}, + election::{ + BallotStyleId, CandidateContest, Contest, Election, PartialElectionHash, PrecinctId, + YesNoContest, + }, +}; + +pub fn encode(election: &Election, encodable_cvr: &EncodableCvr) -> std::io::Result> { + let mut data = Vec::new(); + let mut writer = BitWriter::endian(&mut data, BigEndian); + + encode_into(election, encodable_cvr, &mut writer)?; + writer.byte_align()?; + + Ok(data) +} + +pub fn encode_into( + election: &Election, + encodable_cvr: &EncodableCvr, + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + encode_prelude_into(writer)?; + + // TODO: validate votes + + let cvr = &encodable_cvr.cvr; + let test_mode = &encodable_cvr.test_mode; + let partial_election_hash = encodable_cvr.partial_election_hash.clone(); + + let precinct_id = PrecinctId::from(cvr.ballot_style_unit_id.clone().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "ballot_style_unit_id is missing from CVR", + ) + })?); + let ballot_style_id = BallotStyleId::from(cvr.ballot_style_id.clone().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "ballot_style_id is missing from CVR", + ) + })?); + let contests = election + .get_contests(ballot_style_id.clone()) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("cannot find contests for ballot_style_id={ballot_style_id}"), + ) + })?; + + encode_election_hash_into(&partial_election_hash, writer)?; + encode_ballot_config_into( + election, + ballot_style_id, + precinct_id, + *test_mode, + cvr.vx_ballot_type.clone(), + cvr.unique_id.as_deref(), + writer, + )?; + encode_ballot_votes_into(contests, cvr, writer)?; + + Ok(()) +} + +pub fn encode_prelude_into( + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + // write prelude + writer.write_bytes(b"VX")?; // tag + writer.write(8, ENCODING_VERSION)?; // version + + Ok(()) +} + +pub fn encode_election_hash_into( + partial_election_hash: &PartialElectionHash, + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + writer.write_bytes(&partial_election_hash.to_bytes())?; + + Ok(()) +} + +pub fn encode_ballot_config_into( + election: &Election, + ballot_style_id: BallotStyleId, + precinct_id: PrecinctId, + test_mode: TestMode, + ballot_type: VxBallotType, + ballot_id: Option<&str>, + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + let precinct_id = precinct_id.to_string(); + let ballot_style_id = ballot_style_id.to_string(); + let precinct_index = election + .precincts + .iter() + .position(|precinct| precinct.id.to_string() == precinct_id) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("cannot find precinct with id={}", precinct_id), + ) + })?; + let ballot_style_index = election + .ballot_styles + .iter() + .position(|ballot_style| ballot_style.id.to_string() == ballot_style_id) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("cannot find ballot_style with id={}", ballot_style_id), + ) + })?; + + writer.write(8, election.precincts.len() as u8)?; + writer.write(8, election.ballot_styles.len() as u8)?; + writer.write(8, election.contests.len() as u8)?; + writer.write(sizeof(election.precincts.len() - 1), precinct_index as u32)?; + writer.write( + sizeof(election.ballot_styles.len() - 1), + ballot_style_index as u32, + )?; + writer.write_bit(test_mode == TestMode::Test)?; + writer.write(sizeof(VxBallotType::max() as usize), u32::from(ballot_type))?; + if let Some(ballot_id) = ballot_id { + writer.write_bit(true)?; + writer.write(8, ballot_id.len() as u8)?; + writer.write_bytes(ballot_id.as_bytes())?; + } else { + writer.write_bit(false)?; + } + + Ok(()) +} + +pub fn encode_ballot_votes_into( + contests: Vec<&types_rs::election::Contest>, + cvr: &Cvr, + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + let snapshot = cvr + .cvr_snapshot + .iter() + .find(|snapshot| snapshot.id == cvr.current_snapshot_id) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "cannot find CVR snapshot with id={}", + cvr.current_snapshot_id + ), + ) + })?; + + let all_cvr_contests = snapshot.cvr_contest.as_ref().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "cvr_contest is missing from CVR snapshot", + ) + })?; + + let all_cvr_contests_paired_with_contest_definition = contests + .iter() + .map(|contest| { + let contest_id = contest.id(); + let cvr_contest = all_cvr_contests + .iter() + .find(|cvr_contest| contest_id.to_string() == cvr_contest.contest_id) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("cannot find contest with id={}", contest_id), + ) + })?; + Ok((cvr_contest, contest)) + }) + .collect::>>()?; + + let cvr_contests_with_votes = all_cvr_contests + .iter() + .filter(|cvr_contest| { + cvr_contest + .cvr_contest_selection + .as_ref() + .map(|cvr_contest_selection| { + cvr_contest_selection + .iter() + .flat_map(|selection| &selection.selection_position) + .any(|selection_position| { + selection_position.has_indication == IndicationStatus::Yes + }) + }) + .unwrap_or(false) + }) + .collect::>(); + + // write roll call + for (cvr_contest, _) in all_cvr_contests_paired_with_contest_definition.iter() { + let has_votes = cvr_contests_with_votes.contains(cvr_contest); + writer.write_bit(has_votes)?; + } + + for (cvr_contest, contest) in all_cvr_contests_paired_with_contest_definition.iter() { + if !cvr_contests_with_votes.contains(cvr_contest) { + continue; + } + + let selections = cvr_contest.cvr_contest_selection.as_ref().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "cvr_contest_selection is missing from CVR contest", + ) + })?; + + match contest { + Contest::YesNo(yesno_contest) => { + encode_yesno_selections(yesno_contest, selections, writer)?; + } + Contest::Candidate(candidate_contest) => { + encode_candidate_selections(candidate_contest, selections, writer)?; + } + } + } + + Ok(()) +} + +fn encode_candidate_selections( + candidate_contest: &CandidateContest, + selections: &[CVRContestSelection], + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + // candidate choices get one bit per candidate + let mut non_write_in_count = 0; + for candidate in candidate_contest.candidates.iter() { + let selection = selections + .iter() + .find(|selection| { + selection + .contest_selection_id + .as_ref() + .map(|contest_selection_id| contest_selection_id == &candidate.id.to_string()) + .unwrap_or(false) + }) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "cannot find selection for candidate with id={}", + candidate.id + ), + ) + })?; + let is_selected = selection + .selection_position + .iter() + .any(|selection_position| selection_position.has_indication == IndicationStatus::Yes); + writer.write_bit(is_selected)?; + + if is_selected { + non_write_in_count += 1; + } + } + + if candidate_contest.allow_write_ins { + // write write-in data + let write_in_count = selections + .iter() + .filter(|selection| { + selection + .selection_position + .iter() + .any(|selection_position| { + selection_position.has_indication == IndicationStatus::Yes + && selection_position.cvr_write_in.is_some() + }) + }) + .count(); + let maximum_write_ins = (candidate_contest.seats - non_write_in_count).max(0); + + if maximum_write_ins > 0 { + writer.write(sizeof(maximum_write_ins as usize), write_in_count as u32)?; + + for selection in selections.iter() { + if selection + .selection_position + .iter() + .any(|selection_position| { + selection_position.has_indication == IndicationStatus::Yes + && selection_position.cvr_write_in.is_some() + }) + { + let write_in_name = selection + .selection_position + .iter() + .find_map(|selection_position| { + selection_position + .cvr_write_in + .as_ref() + .map(|cvr_write_in| cvr_write_in.text.as_ref()) + }) + .flatten() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "write-in name is missing from selection", + ) + })?; + encode_write_in_name(write_in_name, writer)?; + } + } + } + } + + Ok(()) +} + +pub fn encode_write_in_name( + write_in_name: &str, + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + if write_in_name.len() > MAXIMUM_WRITE_IN_NAME_LENGTH { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("write-in name is too long: {}", write_in_name.len()), + )); + } + + writer.write( + sizeof(MAXIMUM_WRITE_IN_NAME_LENGTH), + write_in_name.len() as u8, + )?; + + for char in write_in_name.chars() { + let index = WRITE_IN_CHARS.find(char).ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid write-in name char: {char}"), + ) + })?; + writer.write(BITS_PER_WRITE_IN_CHAR, index as u32)?; + } + + Ok(()) +} + +pub fn encode_yesno_selections( + contest: &YesNoContest, + selections: &Vec, + writer: &mut BitWriter<&mut Vec, E>, +) -> std::io::Result<()> { + if selections.len() != 1 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "expected 1 selection for contest with id={}, but got {}", + contest.id, + selections.len() + ), + )); + } + + let selection = &selections[0]; + let is_yes_vote = selection + .contest_selection_id + .as_ref() + .map(|contest_selection_id| contest_selection_id == "yes") + .unwrap_or(false); + + writer.write_bit(is_yes_vote)?; + + Ok(()) +} diff --git a/libs/ballot-encoder-rs/src/lib.rs b/libs/ballot-encoder-rs/src/lib.rs new file mode 100644 index 0000000000..008da7a549 --- /dev/null +++ b/libs/ballot-encoder-rs/src/lib.rs @@ -0,0 +1,9 @@ +mod consts; +mod decode; +mod encode; +mod types; +mod util; + +pub use decode::{decode, decode_header}; +pub use encode::encode; +pub use types::*; diff --git a/libs/ballot-encoder-rs/src/types.rs b/libs/ballot-encoder-rs/src/types.rs new file mode 100644 index 0000000000..fd55ce49ef --- /dev/null +++ b/libs/ballot-encoder-rs/src/types.rs @@ -0,0 +1,39 @@ +use std::str::FromStr; + +use types_rs::{ + cdf::cvr::{Cvr, VxBallotType}, + election::PartialElectionHash, +}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TestMode { + Test, + Live, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EncodableCvr { + pub partial_election_hash: PartialElectionHash, + pub cvr: Cvr, + pub test_mode: TestMode, +} + +#[derive(Debug)] +pub struct BallotConfig { + pub precinct_index: u32, + pub ballot_style_index: u32, + pub ballot_type: VxBallotType, + pub ballot_mode: TestMode, + pub ballot_id: Option, +} + +impl EncodableCvr { + pub fn new(partial_election_hash: PartialElectionHash, cvr: Cvr, test_mode: TestMode) -> Self { + Self { + partial_election_hash: PartialElectionHash::from_str(partial_election_hash.as_str()) + .unwrap_or(partial_election_hash), + cvr, + test_mode, + } + } +} diff --git a/libs/ballot-encoder-rs/src/util.rs b/libs/ballot-encoder-rs/src/util.rs new file mode 100644 index 0000000000..3d1de20b16 --- /dev/null +++ b/libs/ballot-encoder-rs/src/util.rs @@ -0,0 +1,9 @@ +pub(crate) fn sizeof(number: usize) -> u32 { + let mut size = 0; + let mut n = number; + while n > 0 { + size += 1; + n >>= 1; + } + size.max(1) +} diff --git a/libs/ballot-encoder-rs/tests/encoding.rs b/libs/ballot-encoder-rs/tests/encoding.rs new file mode 100644 index 0000000000..6213751597 --- /dev/null +++ b/libs/ballot-encoder-rs/tests/encoding.rs @@ -0,0 +1,219 @@ +use ballot_encoder_rs::{decode, encode, EncodableCvr, TestMode}; +use bitstream_io::{BigEndian, BitWrite, BitWriter}; +use fixtures::encoded::{ + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_VOTES_IN_ALL_CONTESTS, + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_WRITE_INS, +}; +use types_rs::{ + cdf::cvr::{IndicationStatus, VxBallotType}, + election::{BallotStyleId, PrecinctId}, +}; + +use crate::{ + fixtures::{ + encoded::FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_NO_VOTES, read_famous_names_election_definition, + }, + util::{assert_bytes_equal, assert_cvrs_equivalent, build_cvr, sizeof}, +}; + +mod fixtures; +mod util; + +#[test] +fn test_compare_to_manually_encoded_empty_votes() { + let election_definition = read_famous_names_election_definition(); + let election = &election_definition.election; + let election_hash = &election_definition.election_hash; + let ballot_style_id = election.ballot_styles[0].id.clone(); + let contests = election.get_contests(ballot_style_id).unwrap(); + let mut data = vec![]; + let mut writer = BitWriter::endian(&mut data, BigEndian); + // prelude + version number + writer.write_bytes(b"VX").unwrap(); + writer.write(8, 2).unwrap(); + // election hash + let partial_election_hash = election_hash.to_partial(); + dbg!( + partial_election_hash.to_bytes(), + partial_election_hash.to_bytes().len() + ); + writer + .write_bytes(&partial_election_hash.to_bytes()) + .unwrap(); + // check data + writer.write(8, election.precincts.len() as u8).unwrap(); + writer.write(8, election.ballot_styles.len() as u8).unwrap(); + writer.write(8, election.contests.len() as u8).unwrap(); + // precinct index + writer + .write(sizeof(election.precincts.len() - 1), 2) + .unwrap(); + // ballot style index + writer + .write(sizeof(election.ballot_styles.len() - 1), 0) + .unwrap(); + // test ballot? + writer.write_bit(true).unwrap(); + // ballot type + writer + .write( + sizeof(VxBallotType::max() as usize), + u32::from(VxBallotType::Precinct), + ) + .unwrap(); + // ballot id? + writer.write_bit(false).unwrap(); + // vote roll call only, no vote data + for _ in contests { + writer.write_bit(false).unwrap(); + } + writer.byte_align().unwrap(); + + let cvr_with_no_votes = build_cvr( + &election_definition.election, + BallotStyleId::from("1".to_string()), + PrecinctId::from("21".to_string()), + |_, _| (None, IndicationStatus::No), + ); + let encodable_cvr = EncodableCvr::new( + election_definition.election_hash.to_partial(), + cvr_with_no_votes, + TestMode::Test, + ); + assert_bytes_equal( + data.as_slice(), + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_NO_VOTES.as_slice(), + ); + assert_eq!( + encode(&election_definition.election, &encodable_cvr).unwrap(), + data + ); + let decoded_cvr = decode(&election_definition.election, data.as_slice()).unwrap(); + + assert_cvrs_equivalent(&encodable_cvr, &decoded_cvr); + + let round_trip_encoded_cvr = encode(&election_definition.election, &decoded_cvr).unwrap(); + assert_bytes_equal( + round_trip_encoded_cvr.as_slice(), + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_NO_VOTES.as_slice(), + ); +} + +#[test] +fn test_empty_votes_from_machine_encoded() { + let election_definition = read_famous_names_election_definition(); + let cvr_with_no_votes = build_cvr( + &election_definition.election, + BallotStyleId::from("1".to_string()), + PrecinctId::from("21".to_string()), + |_, _| (None, IndicationStatus::No), + ); + let encodable_cvr = EncodableCvr::new( + election_definition.election_hash.to_partial(), + cvr_with_no_votes, + TestMode::Test, + ); + let encoded_cvr = encode(&election_definition.election, &encodable_cvr).unwrap(); + assert_bytes_equal( + encoded_cvr.as_slice(), + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_NO_VOTES.as_slice(), + ); + + let decoded_cvr = decode(&election_definition.election, encoded_cvr.as_slice()).unwrap(); + assert_cvrs_equivalent(&encodable_cvr, &decoded_cvr); +} + +#[test] +fn test_votes_from_machine_encoded() { + let election_definition = read_famous_names_election_definition(); + let cvr_with_votes = build_cvr( + &election_definition.election, + BallotStyleId::from("1".to_string()), + PrecinctId::from("21".to_string()), + |_, option_id| match option_id { + "thomas-edison" + | "winston-churchill" + | "mark-twain" + | "bill-nye" + | "alfred-hitchcock" + | "johan-sebastian-bach" + | "nikola-tesla" + | "jackie-chan" + | "tim-allen" + | "harriet-tubman" + | "marilyn-monroe" => (None, IndicationStatus::Yes), + _ => (None, IndicationStatus::No), + }, + ); + let encodable_cvr = EncodableCvr::new( + election_definition.election_hash.to_partial(), + cvr_with_votes, + TestMode::Test, + ); + assert_bytes_equal( + encode(&election_definition.election, &encodable_cvr) + .unwrap() + .as_slice(), + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_VOTES_IN_ALL_CONTESTS.as_slice(), + ); + assert_cvrs_equivalent( + &decode( + &election_definition.election, + &FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_VOTES_IN_ALL_CONTESTS, + ) + .unwrap(), + &encodable_cvr, + ); +} + +#[test] +fn test_votes_from_machine_encoded_with_write_ins() { + let election_definition = read_famous_names_election_definition(); + let cvr_with_votes = build_cvr( + &election_definition.election, + BallotStyleId::from("1".to_string()), + PrecinctId::from("22".to_string()), + |contest_id, option_id| match (contest_id.to_string().as_str(), option_id) { + (_, "thomas-edison") + | (_, "oprah-winfrey") + | (_, "john-snow") + | (_, "bill-nye") + | (_, "vincent-van-gogh") + | (_, "wolfgang-amadeus-mozart") + | (_, "tim-allen") + | (_, "harriet-tubman") => (None, IndicationStatus::Yes), + ("chief-of-police", "write-in-0") => { + (Some("MERLIN".to_string()), IndicationStatus::Yes) + } + ("parks-and-recreation-director", "write-in-0") => ( + Some(r#"QWERTYUIOPASDFGHJKL'"ZXCVBNM,.- "'.,-POI"#.to_string()), + IndicationStatus::Yes, + ), + ("board-of-alderman", "write-in-0") => { + (Some("JOHN".to_string()), IndicationStatus::Yes) + } + ("board-of-alderman", "write-in-1") => { + (Some("ALICE".to_string()), IndicationStatus::Yes) + } + _ => (None, IndicationStatus::No), + }, + ); + let encodable_cvr = EncodableCvr::new( + election_definition.election_hash.to_partial(), + cvr_with_votes, + TestMode::Test, + ); + let decoded = decode( + &election_definition.election, + &FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_WRITE_INS, + ) + .unwrap(); + + assert_cvrs_equivalent(&decoded, &encodable_cvr); + assert_bytes_equal( + encode(&election_definition.election, &encodable_cvr) + .unwrap() + .as_slice(), + FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_WRITE_INS.as_slice(), + ); +} diff --git a/libs/ballot-encoder-rs/tests/fixtures/electionFamousNames2021.json b/libs/ballot-encoder-rs/tests/fixtures/electionFamousNames2021.json new file mode 100755 index 0000000000..3e7a46c1fe --- /dev/null +++ b/libs/ballot-encoder-rs/tests/fixtures/electionFamousNames2021.json @@ -0,0 +1,324 @@ +{ + "title": "Lincoln Municipal General Election", + "state": "State of Hamilton", + "county": { + "id": "franklin", + "name": "Franklin County" + }, + "date": "2021-06-06T00:00:00-10:00", + "parties": [ + { + "id": "0", + "name": "Democrat", + "fullName": "Democratic Party", + "abbrev": "D" + }, + { + "id": "1", + "name": "Republican", + "fullName": "Republican Party", + "abbrev": "R" + }, + { + "id": "2", + "name": "Liberty", + "fullName": "Liberty Party", + "abbrev": "Li" + }, + { + "id": "3", + "name": "Green", + "fullName": "Green Party", + "abbrev": "G" + } + ], + "contests": [ + { + "id": "mayor", + "districtId": "district-1", + "type": "candidate", + "title": "Mayor", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "sherlock-holmes", + "name": "Sherlock Holmes", + "partyIds": ["0"] + }, + { + "id": "thomas-edison", + "name": "Thomas Edison", + "partyIds": ["1"] + } + ] + }, + { + "id": "controller", + "districtId": "district-1", + "type": "candidate", + "title": "Controller", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "winston-churchill", + "name": "Winston Churchill", + "partyIds": ["0"] + }, + { + "id": "oprah-winfrey", + "name": "Oprah Winfrey", + "partyIds": ["1"] + }, + { + "id": "louis-armstrong", + "name": "Louis Armstrong", + "partyIds": ["3"] + } + ] + }, + { + "id": "attorney", + "districtId": "district-1", + "type": "candidate", + "title": "Attorney", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "john-snow", + "name": "John Snow", + "partyIds": ["1"] + }, + { + "id": "mark-twain", + "name": "Mark Twain", + "partyIds": ["3"] + } + ] + }, + { + "id": "public-works-director", + "districtId": "district-1", + "type": "candidate", + "title": "Public Works Director", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "benjamin-franklin", + "name": "Benjamin Franklin", + "partyIds": ["0"] + }, + { + "id": "robert-downey-jr", + "name": "Robert Downey Jr.", + "partyIds": ["1"] + }, + { + "id": "bill-nye", + "name": "Bill Nye", + "partyIds": ["3"] + } + ] + }, + { + "id": "chief-of-police", + "districtId": "district-1", + "type": "candidate", + "title": "Chief of Police", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "natalie-portman", + "name": "Natalie Portman", + "partyIds": ["0"] + }, + { + "id": "frank-sinatra", + "name": "Frank Sinatra", + "partyIds": ["1"] + }, + { + "id": "andy-warhol", + "name": "Andy Warhol", + "partyIds": ["3"] + }, + { + "id": "alfred-hitchcock", + "name": "Alfred Hitchcock", + "partyIds": ["3"] + } + ] + }, + { + "id": "parks-and-recreation-director", + "districtId": "district-1", + "type": "candidate", + "title": "Parks and Recreation Director", + "seats": 1, + "allowWriteIns": true, + "candidates": [ + { + "id": "charles-darwin", + "name": "Charles Darwin", + "partyIds": ["0"] + }, + { + "id": "stephen-hawking", + "name": "Stephen Hawking", + "partyIds": ["1"] + }, + { + "id": "johan-sebastian-bach", + "name": "Johann Sebastian Bach", + "partyIds": ["0"] + }, + { + "id": "alexander-graham-bell", + "name": "Alexander Graham Bell", + "partyIds": ["1"] + } + ] + }, + { + "id": "board-of-alderman", + "districtId": "district-1", + "type": "candidate", + "title": "Board of Alderman", + "seats": 4, + "allowWriteIns": true, + "candidates": [ + { + "id": "helen-keller", + "name": "Helen Keller", + "partyIds": ["0"] + }, + { + "id": "steve-jobs", + "name": "Steve Jobs", + "partyIds": ["1"] + }, + { + "id": "nikola-tesla", + "name": "Nikola Tesla", + "partyIds": ["0"] + }, + { + "id": "vincent-van-gogh", + "name": "Vincent Van Gogh", + "partyIds": ["1"] + }, + { + "id": "pablo-picasso", + "name": "Pablo Picasso", + "partyIds": ["1"] + }, + { + "id": "wolfgang-amadeus-mozart", + "name": "Wolfgang Amadeus Mozart", + "partyIds": ["2"] + } + ] + }, + { + "id": "city-council", + "districtId": "district-1", + "type": "candidate", + "title": "City Council", + "seats": 4, + "allowWriteIns": true, + "candidates": [ + { + "id": "marie-curie", + "name": "Marie Curie", + "partyIds": ["0"] + }, + { + "id": "indiana-jones", + "name": "Indiana Jones", + "partyIds": ["1"] + }, + { + "id": "mona-lisa", + "name": "Mona Lisa", + "partyIds": ["3"] + }, + { + "id": "jackie-chan", + "name": "Jackie Chan", + "partyIds": ["3"] + }, + { + "id": "tim-allen", + "name": "Tim Allen", + "partyIds": ["2"] + }, + { + "id": "mark-antony", + "name": "Mark Antony", + "partyIds": ["0"] + }, + { + "id": "harriet-tubman", + "name": "Harriet Tubman", + "partyIds": ["1"] + }, + { + "id": "martin-luther-king", + "name": "Dr. Martin Luther King Jr.", + "partyIds": ["0"] + }, + { + "id": "marilyn-monroe", + "name": "Marilyn Monroe", + "partyIds": ["1"] + } + ] + } + ], + "districts": [ + { + "id": "district-1", + "name": "City of Lincoln" + } + ], + "precincts": [ + { + "id": "23", + "name": "North Lincoln" + }, + { + "id": "22", + "name": "South Lincoln" + }, + { + "id": "21", + "name": "East Lincoln" + }, + { + "id": "20", + "name": "West Lincoln" + } + ], + "ballotStyles": [ + { + "id": "1", + "precincts": ["20", "21", "22", "23"], + "districts": ["district-1"] + } + ], + "sealUrl": "/seals/state-of-hamilton-official-seal.svg", + "adjudicationReasons": [ + "UninterpretableBallot", + "Overvote", + "Undervote", + "BlankBallot" + ], + "markThresholds": { + "definite": 0.12, + "marginal": 0.12 + } +} diff --git a/libs/ballot-encoder-rs/tests/fixtures/encoded.rs b/libs/ballot-encoder-rs/tests/fixtures/encoded.rs new file mode 100644 index 0000000000..3b133a73df --- /dev/null +++ b/libs/ballot-encoder-rs/tests/fixtures/encoded.rs @@ -0,0 +1,320 @@ +#[allow(clippy::unusual_byte_groupings)] +pub const FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_NO_VOTES: [u8; 19] = [ + // prelude + b'V', + b'X', + 0x02, // version + // election hash + 0xb4, + 0xe0, + 0x78, + 0x14, + 0xb4, + 0x69, + 0x11, + 0x21, + 0x1e, + 0xc7, + // check data + 0x04, // precinct count + 0x01, // ballot style count + 0x08, // contest count + // ballot config + 0b10_0_1_0000, + //││ │ │ ││││ + //││ │ │ └┴┴┴── ballot type (precinct=0) + //││ │ └── test ballot (true) + //││ └── ballot style index (0) + //└┴── precinct index (2) + 0b0_0000000, + //│ │││││││ + //│ └┴┴┴┴┴┴── vote roll call #0-6 (all false) + //└── ballot id present? (false) + 0b0_0000000, + //│ │││││││ + //│ └┴┴┴┴┴┴── padding bits + //└── vote roll call #7 (false) +]; + +#[allow(clippy::unusual_byte_groupings)] +pub const FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_VOTES_IN_ALL_CONTESTS: [u8; 23] = [ + // prelude + b'V', + b'X', + 0x02, // version + // election hash + 0xb4, + 0xe0, + 0x78, + 0x14, + 0xb4, + 0x69, + 0x11, + 0x21, + 0x1e, + 0xc7, + // check data + 0x04, // precinct count + 0x01, // ballot style count + 0x08, // contest count + // ballot config + 0b10_0_1_0000, + //││ │ │ ││││ + //││ │ │ └┴┴┴── ballot type (precinct=0) + //││ │ └── test ballot (true) + //││ └── ballot style index (0) + //└┴── precinct index (2) + 0b0_1111111, + //│ │││││││ + //│ └┴┴┴┴┴┴── vote roll call #0-6 (all true) + //└ ballot id present? (false) + 0b1_01_100_01, + //│ ││ │││ ││ + //│ ││ │││ └┴── "attorney" votes (john-snow=no, mark-twain=yes) + //│ ││ └┴┴── "controller" votes (winston-churchill=yes, oprah-winfrey=no, louis-armstrong=no) + //│ └┴── "mayor" votes (sherlock-holmes=no, thomas-edison=yes) + //└── vote roll call #7 (true) + 0b001_0001_0, + //│││ ││││ │ + //│││ ││││ └── "parks-and-recreation-director" vote #0 (charles-darwin=no) + //│││ └┴┴┴── "chief-of-police" votes (natalie-portman=no, frank-sinatra=no, andy-warhol=no, alfred-hitchcock=yes) + //└┴┴── "public-works-director" votes (benjamin-franklin=no, robert-downey-jr=no, bill-nye=yes) + 0b010_00100, + //│││ │││││ + //│││ └┴┴┴┴── "board-of-alderman" votes #0-4 (helen-keller=no, steve-jobs=no, nikola-tesla=yes, vincent-van-gogh=no, pablo-picasso=no) + //└┴┴── "parks-and-recreation-director" votes #1-3 (stephen-hawking=no, johan-sebastian-bach=yes, alexander-graham-bell=no) + 0b0_00_00011, + //│ ││ │││││ + //│ ││ └┴┴┴┴── "city-council" votes #0-4 (marie-curie=no, indiana-jones=no, mona-lisa=no, jackie-chan=yes, tim-allen=yes) + //│ └┴── "board-of-alderman" write-in count (0) + //└── "board-of-alderman" vote #5 (wolfgang-amadeus-mozart=no) + 0b0101_0000, + //││││ ││││ + //││││ └┴┴┴── padding bits + //└┴┴┴── "city-council" votes #5-8 (mark-antony=no, harriet-tubman=yes, martin-luther-king=no, marilyn-monroe=yes) +]; + +#[allow(clippy::unusual_byte_groupings)] +pub const FAMOUS_NAMES_EAST_LINCOLN_STYLE_1_WRITE_INS: [u8; 61] = [ + // prelude + b'V', + b'X', + 0x02, // version + // election hash + 0xb4, + 0xe0, + 0x78, + 0x14, + 0xb4, + 0x69, + 0x11, + 0x21, + 0x1e, + 0xc7, + // check data + 0x04, // precinct count + 0x01, // ballot style count + 0x08, // contest count + // ballot config + 0b01_0_1_0000, + //││ │ │ ││││ + //││ │ │ └┴┴┴── ballot type (precinct=0) + //││ │ └── test ballot (true) + //││ └── ballot style index (0) + //└┴── precinct index (1) + 0b0_1111111, + //│ │││││││ + //│ └┴┴┴┴┴┴── vote roll call #0-6 (all true) + //└ ballot id present? (false) + 0b1_01_010_10, + //│ ││ │││ ││ + //│ ││ │││ └┴── "attorney" votes (john-snow=yes, mark-twain=no) + //│ ││ └┴┴── "controller" votes (winston-churchill=no, oprah-winfrey=yes, louis-armstrong=no) + //│ └┴── "mayor" votes (sherlock-holmes=no, thomas-edison=yes) + //└── vote roll call #7 (true) + 0b001_0000_1, + //│││ ││││ │ + //│││ ││││ └── "chief-of-police" write-in count (1) + //│││ └┴┴┴── "chief-of-police" votes (natalie-portman=no, frank-sinatra=no, andy-warhol=no, alfred-hitchcock=no) + //└┴┴── "public-works-director" votes (benjamin-franklin=no, robert-downey-jr=no, bill-nye=yes) + 0b000110_01, + //││││││ ││ + //││││││ └┴── "chief-of-police" write-in #0 char #0 (M) bits #0-1 + //└┴┴┴┴┴── "chief-of-police" write-in #0 name length (6) + 0b100_00100, + //│││ │││││ + //│││ └┴┴┴┴── "chief-of-police" write-in #0 char #1 (E) bits #0-4 + //└┴┴── "chief-of-police" write-in #0 char #0 (M) bits #2-4 + 0b10001_010, + //│││││ │││ + //│││││ └┴┴── "chief-of-police" write-in #0 char #3 (L) bits #0-2 + //└┴┴┴┴── "chief-of-police" write-in #0 char #2 (R) bits #0-4 + 0b11_01000_0, + //││ │││││ │ + //││ │││││ └── "chief-of-police" write-in #0 char #5 (N) bit #0 + //││ └┴┴┴┴── "chief-of-police" write-in #0 char #4 (I) bits #0-4 + //└┴── "chief-of-police" write-in #0 char #3 (L) bits #3-4 + 0b1101_0000, + //││││ ││││ + //││││ └┴┴┴── "parks-and-recreation-director" votes (charles-darwin=no, stephen-hawking=no, johan-sebastian-bach=no, alexander-graham-bell=no) + //└┴┴┴── "chief-of-police" write-in #0 char #5 (N) bits #1-4 + 0b1_101000_1, + //│ ││││││ │ + //│ ││││││ └── "parks-and-recreation-director" write-in #0 char #0 (Q) bit #0 + //│ ││││││ + //│ └┴┴┴┴┴── "parks-and-recreation-director" write-in #0 length (40) + //└── "parks-and-recreation-director" write-in count (1) + 0b0000_1011, + //││││ ││││ + //││││ └┴┴┴── "parks-and-recreation-director" write-in #0 char #1 (W) bits #0-3 + //└┴┴┴── "parks-and-recreation-director" write-in #0 char #0 (Q) bits #1-4 + 0b0_00100_10, + //│ │││││ ││ + //│ │││││ └┴── "parks-and-recreation-director" write-in #0 char #3 (R) bits #0-1 + //│ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #2 (E) bits #0-4 + //└── "parks-and-recreation-director" write-in #0 char #1 (W) bit #4 + 0b001_10011, + //│││ │││││ + //│││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #4 (T) bits #0-4 + //└┴┴── "parks-and-recreation-director" write-in #0 char #3 (R) bits #2-4 + 0b11000_101, + //│││││ │││ + //│││││ └┴┴── "parks-and-recreation-director" write-in #0 char #6 (U) bits #0-2 + //└┴┴┴┴── "parks-and-recreation-director" write-in #0 char #5 (Y) bits #0-4 + 0b00_01000_0, + //││ │││││ │ + //││ │││││ └── "parks-and-recreation-director" write-in #0 char #8 (O) bit #0 + //││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #7 (I) bits #0-4 + //└┴── "parks-and-recreation-director" write-in #0 char #6 (U) bits #2-4 + 0b1110_0111, + //││││ ││││ + //││││ └┴┴┴── "parks-and-recreation-director" write-in #0 char #9 (P) bits #0-3 + //└┴┴┴── "parks-and-recreation-director" write-in #0 char #8 (O) bits #1-4 + 0b1_00000_10, + //│ │││││ ││ + //│ │││││ └┴── "parks-and-recreation-director" write-in #0 char #11 (S) bits #0-1 + //│ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #10 (A) bits #0-4 + //└── "parks-and-recreation-director" write-in #0 char #9 (P) bit #4 + 0b010_00011, + //│││ │││││ + //│││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #12 (D) bits #0-4 + //└┴┴── "parks-and-recreation-director" write-in #0 char #11 (S) bits #2-4 + 0b00101_001, + //│││││ │││ + //│││││ └┴┴── "parks-and-recreation-director" write-in #0 char #14 (G) bits #0-2 + //└┴┴┴┴── "parks-and-recreation-director" write-in #0 char #13 (F) bits #0-4 + 0b10_00111_0, + //││ │││││ │ + //││ │││││ └── "parks-and-recreation-director" write-in #0 char #16 (J) bit #0 + //││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #15 (H) bits #0-4 + //└┴── "parks-and-recreation-director" write-in #0 char #14 (G) bits #2-4 + 0b1001_0101, + //││││ ││││ + //││││ └┴┴┴── "parks-and-recreation-director" write-in #0 char #17 (K) bits #0-3 + //└┴┴┴── "parks-and-recreation-director" write-in #0 char #16 (J) bits #1-4 + 0b0_01011_11, + //│ │││││ ││ + //│ │││││ └┴── "parks-and-recreation-director" write-in #0 char #19 (') bits #0-1 + //│ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #18 (L) bits #0-4 + //└── "parks-and-recreation-director" write-in #0 char #17 (K) bit #4 + 0b011_11100, + //│││ │││││ + //│││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #20 (") bits #0-4 + //└┴┴── "parks-and-recreation-director" write-in #0 char #19 (') bits #2-4 + 0b11001_101, + //│││││ │││ + //│││││ └┴┴── "parks-and-recreation-director" write-in #0 char #22 (X) bits #0-2 + //└┴┴┴┴── "parks-and-recreation-director" write-in #0 char #21 (Z) bits #0-4 + 0b11_00010_1, + //││ │││││ │ + //││ │││││ └── "parks-and-recreation-director" write-in #0 char #24 (V) bit #0 + //││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #23 (C) bits #0-4 + //└┴── "parks-and-recreation-director" write-in #0 char #22 (X) bits #2-4 + 0b0101_0000, + //││││ ││││ + //││││ └┴┴┴── "parks-and-recreation-director" write-in #0 char #25 (B) bits #0-3 + //└┴┴┴── "parks-and-recreation-director" write-in #0 char #24 (V) bits #1-4 + 0b1_01101_01, + //│ │││││ ││ + //│ │││││ └┴── "parks-and-recreation-director" write-in #0 char #27 (M) bits #0-1 + //│ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #26 (N) bits #0-4 + //└── "parks-and-recreation-director" write-in #0 char #25 (B) bit #4 + 0b100_11111, + //│││ │││││ + //│││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #28 (,) bits #0-4 + //└┴┴── "parks-and-recreation-director" write-in #0 char #27 (M) bits #2-4 + 0b11110_111, + //│││││ │││ + //│││││ └┴┴── "parks-and-recreation-director" write-in #0 char #30 (-) bits #0-2 + //└┴┴┴┴── "parks-and-recreation-director" write-in #0 char #29 (.) bits #0-4 + 0b01_11010_1, + //││ │││││ │ + //││ │││││ └── "parks-and-recreation-director" write-in #0 char #32 (") bit #0 + //││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #31 ( ) bits #0-4 + //└┴── "parks-and-recreation-director" write-in #0 char #30 (-) bits #2-4 + 0b1100_1101, + //││││ ││││ + //││││ └┴┴┴── "parks-and-recreation-director" write-in #0 char #33 (') bits #0-3 + //└┴┴┴── "parks-and-recreation-director" write-in #0 char #32 (") bits #1-4 + 0b1_11110_11, + //│ │││││ ││ + //│ │││││ └┴── "parks-and-recreation-director" write-in #0 char #35 (,) bits #0-1 + //│ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #34 (.) bits #0-4 + //└── "parks-and-recreation-director" write-in #0 char #33 (') bit #4 + 0b111_11101, + //│││ │││││ + //│││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #36 (-) bits #0-4 + //└┴┴── "parks-and-recreation-director" write-in #0 char #35 (,) bits #2-4 + 0b01111_011, + //│││││ │││ + //│││││ └┴┴── "parks-and-recreation-director" write-in #0 char #38 (O) bits #0-2 + //└┴┴┴┴── "parks-and-recreation-director" write-in #0 char #37 (P) bits #0-4 + 0b10_01000_0, + //││ │││││ │ + //││ │││││ └── "board-of-alderman" vote #0 (helen-keller=no) + //││ └┴┴┴┴── "parks-and-recreation-director" write-in #0 char #39 (I) bits #0-4 + //└┴── "parks-and-recreation-director" write-in #0 char #38 (O) bits #2-4 + 0b00101_10_0, + //│││││ ││ │ + //│││││ ││ └── "board-of-alderman" write-in #0 length (4) bit #0 + //│││││ └┴── "board-of-alderman" write-in count (2) + //└┴┴┴┴── "board-of-alderman" votes #1-3 (steve-jobs=no, nikola-tesla=no, vincent-van-gogh=yes, pablo-picasso=no, wolfgang-amadeus-mozart=no) + 0b00100_010, + //│││││ │││ + //│││││ └┴┴── "board-of-alderman" write-in #0 char #0 (J) bits #0-2 + //└┴┴┴┴── "board-of-alderman" write-in #0 length (4) bits #1-5 + 0b01_01110_0, + //││ │││││ │ + //││ │││││ └── "board-of-alderman" write-in #0 char #2 (H) bit #0 + //││ └┴┴┴┴── "board-of-alderman" write-in #0 char #1 (O) bits #0-4 + //└┴── "board-of-alderman" write-in #0 char #0 (J) bits #3-4 + 0b0111_0110, + //││││ ││││ + //││││ └┴┴┴── "board-of-alderman" write-in #0 char #3 (N) bits #0-3 + //└┴┴┴── "board-of-alderman" write-in #0 char #2 (H) bits #1-4 + 0b1_000101_0, + //│ ││││││ │ + //│ ││││││ └── "board-of-alderman" write-in #1 char #0 (A) bit #0 + //│ └┴┴┴┴┴── "board-of-alderman" write-in #1 length (5) + //└── "board-of-alderman" write-in #0 char #3 (N) bit #4 + 0b0000_0101, + //││││ ││││ + //││││ └┴┴┴── "board-of-alderman" write-in #1 char #1 (L) bits #0-3 + //└┴┴┴── "board-of-alderman" write-in #1 char #0 (A) bits #1-4 + 0b1_01000_00, + //│ │││││ ││ + //│ │││││ └┴── "board-of-alderman" write-in #1 char #3 (C) bits #0-1 + //│ └┴┴┴┴── "board-of-alderman" write-in #1 char #2 (I) bits #0-4 + //└── "board-of-alderman" write-in #1 char #1 (L) bit #4 + 0b010_00100, + //│││ │││││ + //│││ └┴┴┴┴── "board-of-alderman" write-in #1 char #4 (E) bits #0-4 + //└┴┴── "board-of-alderman" write-in #1 char #3 (C) bits #2-4 + 0b00001010, + //││││││││ + //└┴┴┴┴┴┴┴── "city-council" votes #0-7 (marie-curie=no, indiana-jones=no, mona-lisa=no, jackie-chan=no, tim-allen=yes, mark-antony=no, harriet-tubman=yes, martin-luther-king=no, marilyn-monroe=no) + 0b00_000000, + //││ ││││││ + //││ └┴┴┴┴┴── padding bits + //└┴── "city-council" write-in count (0) +]; diff --git a/libs/ballot-encoder-rs/tests/fixtures/mod.rs b/libs/ballot-encoder-rs/tests/fixtures/mod.rs new file mode 100644 index 0000000000..a4d9628664 --- /dev/null +++ b/libs/ballot-encoder-rs/tests/fixtures/mod.rs @@ -0,0 +1,9 @@ +use types_rs::election::ElectionDefinition; + +pub mod encoded; + +pub fn read_famous_names_election_definition() -> ElectionDefinition { + include_str!("./electionFamousNames2021.json") + .parse::() + .unwrap() +} diff --git a/libs/ballot-encoder-rs/tests/util/mod.rs b/libs/ballot-encoder-rs/tests/util/mod.rs new file mode 100644 index 0000000000..db35842fbf --- /dev/null +++ b/libs/ballot-encoder-rs/tests/util/mod.rs @@ -0,0 +1,152 @@ +use ballot_encoder_rs::EncodableCvr; +use nanoid::nanoid; +use pretty_assertions::assert_eq; +use types_rs::cdf::cvr::{ + CVRContest, CVRContestSelection, CVRSnapshot, CVRWriteIn, Cvr, IndicationStatus, + SelectionPosition, +}; +use types_rs::election::{BallotStyleId, Contest, ContestId, Election, PrecinctId}; + +pub fn build_cvr( + election: &Election, + ballot_style_id: BallotStyleId, + precinct_id: PrecinctId, + get_indication_status: impl Fn(&ContestId, &str) -> (Option, IndicationStatus), +) -> Cvr { + let id = nanoid!(); + let contests = election + .get_contests(ballot_style_id.clone()) + .unwrap() + .iter() + .map(|contest| { + let contest_id = contest.id().to_string(); + let contest_selection_ids = contest + .option_ids() + .iter() + .map(|option_id| option_id.to_string()) + .collect::>(); + build_cvr_contest( + ContestId::from(contest_id), + contest_selection_ids.iter().map(|s| s.as_str()).collect(), + match contest { + Contest::YesNo(_) => 0, + Contest::Candidate(candidate_contest) => { + if candidate_contest.allow_write_ins { + candidate_contest.seats as usize + } else { + 0 + } + } + }, + &get_indication_status, + ) + }) + .collect(); + Cvr { + ballot_style_id: Some(ballot_style_id.to_string()), + ballot_style_unit_id: Some(precinct_id.to_string()), + current_snapshot_id: id.clone(), + cvr_snapshot: vec![CVRSnapshot { + id, + cvr_contest: Some(contests), + ..Default::default() + }], + ..Default::default() + } +} + +pub fn assert_cvrs_equivalent(left: &EncodableCvr, right: &EncodableCvr) { + let mut left: EncodableCvr = (*left).clone(); + + // check consistency + assert_eq!(left.cvr.current_snapshot_id, left.cvr.cvr_snapshot[0].id); + assert_eq!(right.cvr.current_snapshot_id, right.cvr.cvr_snapshot[0].id); + + // update the snapshot IDs to match + left.cvr.current_snapshot_id = right.cvr.current_snapshot_id.clone(); + left.cvr.cvr_snapshot[0].id = right.cvr.cvr_snapshot[0].id.clone(); + + assert_eq!(left, *right); +} + +pub fn assert_bytes_equal(left: &[u8], right: &[u8]) { + assert_eq!(pretty_bytes(left), pretty_bytes(right)); +} + +fn pretty_bytes(bytes: &[u8]) -> String { + let mut binary_string = String::new(); + for byte in bytes.iter() { + let char = char::from(*byte); + binary_string.push_str(&format!( + "{:08b} {:02x?} {}\n", + byte, + byte, + if char.is_alphanumeric() { char } else { ' ' }, + )); + } + binary_string +} + +fn build_cvr_contest( + contest_id: ContestId, + contest_selection_ids: Vec<&str>, + possible_write_in_count: usize, + get_indication_status: impl Fn(&ContestId, &str) -> (Option, IndicationStatus), +) -> CVRContest { + let write_in_contest_selections = (0..possible_write_in_count).flat_map(|index| { + let (write_in_name, has_indication) = + get_indication_status(&contest_id, &format!("write-in-{}", index)); + + write_in_name.map(|write_in_name| CVRContestSelection { + contest_selection_id: None, + option_position: None, + selection_position: vec![SelectionPosition { + has_indication, + cvr_write_in: Some(CVRWriteIn { + text: Some(write_in_name), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }) + }); + CVRContest { + contest_id: contest_id.to_string(), + cvr_contest_selection: Some( + contest_selection_ids + .iter() + .enumerate() + .map(|(option_position, contest_selection_id)| { + let (write_in_name, has_indication) = + get_indication_status(&contest_id, contest_selection_id); + CVRContestSelection { + contest_selection_id: Some(contest_selection_id.to_string()), + option_position: Some(option_position as i64), + selection_position: vec![SelectionPosition { + has_indication, + cvr_write_in: write_in_name.map(|text| CVRWriteIn { + text: Some(text), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + } + }) + .chain(write_in_contest_selections) + .collect(), + ), + ..Default::default() + } +} + +pub fn sizeof(number: usize) -> u32 { + let mut size = 0; + let mut n = number; + while n > 0 { + size += 1; + n >>= 1; + } + size.max(1) +} diff --git a/libs/basics/src/errors.test.ts b/libs/basics/src/errors.test.ts index 9e85b52e08..3ee1788da3 100644 --- a/libs/basics/src/errors.test.ts +++ b/libs/basics/src/errors.test.ts @@ -12,6 +12,7 @@ test.each<{ error: unknown; expectedErrorMessage: string }>([ { error: Buffer.from('Whoa!', 'utf-8'), expectedErrorMessage: 'Whoa!' }, { error: 1234, expectedErrorMessage: '1234' }, { error: { error: 'Whoa!' }, expectedErrorMessage: '[object Object]' }, + { error: { message: 'Whoa!' }, expectedErrorMessage: 'Whoa!' }, ])('extractErrorMessage', ({ error, expectedErrorMessage }) => { expect(extractErrorMessage(error)).toEqual(expectedErrorMessage); }); diff --git a/libs/basics/src/errors.ts b/libs/basics/src/errors.ts index 80e4469463..5629fcd5bf 100644 --- a/libs/basics/src/errors.ts +++ b/libs/basics/src/errors.ts @@ -2,7 +2,18 @@ * Extracts the error message from an error in a type-safe way */ export function extractErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); + return error instanceof Error + ? error.message + : typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as { message?: unknown }).message === 'string' + ? (error as { message: string }).message + : error && + typeof error === 'object' && + 'toString' in (error as { toString(): string }) + ? (error as { toString(): string }).toString() + : String(error); } /** diff --git a/libs/central-scanner/Cargo.toml b/libs/central-scanner/Cargo.toml new file mode 100644 index 0000000000..35bec95d9a --- /dev/null +++ b/libs/central-scanner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "central-scanner" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +color-eyre = { workspace = true } +env_logger = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } diff --git a/libs/central-scanner/package.json b/libs/central-scanner/package.json new file mode 100644 index 0000000000..74aa50bba8 --- /dev/null +++ b/libs/central-scanner/package.json @@ -0,0 +1,16 @@ +{ + "name": "central-scanner", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "cargo build", + "lint": "cargo clippy -- -D warnings", + "start": "cargo watch -x run", + "test": "cargo test" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "packageManager": "pnpm@8.1.0" +} diff --git a/libs/central-scanner/src/fujitsu.rs b/libs/central-scanner/src/fujitsu.rs new file mode 100644 index 0000000000..dd9dfedf65 --- /dev/null +++ b/libs/central-scanner/src/fujitsu.rs @@ -0,0 +1,280 @@ +use std::fmt::{self, Debug, Formatter}; +use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::process::{Child, ChildStdin, ChildStdout, Stdio}; +use std::sync::mpsc::{self, Receiver, SyncSender}; +use std::{path::PathBuf, process::Command}; + +use color_eyre::eyre::eyre; +use log::{debug, error, info}; + +/// Start a new scan session. +/// +/// # Example +/// +/// ```rust +/// use std::path::PathBuf; +/// use central_scanner::scan; +/// +/// fn do_scan() -> Result<(), Box> { +/// let session = scan(PathBuf::from("/tmp"))?; +/// for (side_a_path, side_b_path) in session { +/// println!("side_a_path: {:?}", side_a_path); +/// println!("side_b_path: {:?}", side_b_path); +/// } +/// Ok(()) +/// } +/// ``` +pub fn scan(directory: PathBuf) -> io::Result { + ScanSession::start(directory) +} + +/// Internal actions that control the `scanimage` process. +#[derive(Debug)] +pub enum Action { + /// Request a new card be scanned. + Scan, + + /// Stop the scan session. + Stop, +} + +/// Represents a scanned card, i.e. a sheet of paper. +pub type Card = (PathBuf, PathBuf); + +/// A scan session manages the `scanimage` process and handles requests to scan +/// cards. +pub struct ScanSession { + /// The directory where the scanned images are stored. Must be writable by + /// the current user. + directory: PathBuf, + + /// The `scanimage` process. + scanimage: std::process::Child, + + /// The channel used to send scan requests to the `scanimage` process. + action_tx: SyncSender, + + /// The channel used to receive scanned cards from the `scanimage` process. + card_rx: Receiver<(String, String)>, +} + +impl Debug for ScanSession { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("ScanSession") + .field("directory", &self.directory) + .finish() + } +} + +/// Start the `scanimage` process in batch mode with all the expected options. +fn start_scanimage(batch_template: &str) -> io::Result { + info!("starting scanimage with batch template: {}", batch_template); + Command::new("scanimage") + .arg("-d") + .arg("fujitsu") + .arg("--resolution") + .arg("200") + .arg("--format=jpeg") + .arg("--source=ADF Duplex") + .arg("--dropoutcolor") + .arg("Red") + .arg(format!("--batch={batch_template}")) + .arg("--batch-print") + .arg("--batch-prompt") + .arg("--mode") + .arg("gray") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() +} + +/// Loops over `lines` until it finds the line containing ``. This line +/// indicates that `scanimage` is ready to scan. +fn block_until_scanimage_ready(lines: &mut I) -> io::Result<()> +where + I: Iterator>, +{ + debug!("[scanimage] blocking until ready to scan"); + for line in lines { + let line = line?; + debug!("[scanimage] stderr: {}", line); + if line.contains("") { + debug!("[scanimage] ready"); + break; + } + } + Ok(()) +} + +/// Sends the lines from `lines` to the log as debug messages. +fn pipe_stderr_to_log(lines: I) +where + I: Iterator> + Send + 'static, +{ + std::thread::spawn(move || { + for line in lines.flatten() { + debug!("[scanimage] stderr: {}", line); + } + + debug!("[scanimage] stderr closed"); + }); +} + +/// Establish a duplex channel between the `scanimage` process and the calling +/// thread. The calling thread will send `Action` values to the `scanimage` +/// process and receive scanned cards. +fn establish_batch_scanimage_duplex_channel( + stdin: ChildStdin, + stdout: ChildStdout, +) -> (SyncSender, Receiver<(String, String)>) { + let (card_tx, card_rx) = mpsc::sync_channel::<(String, String)>(1); + let (action_tx, action_rx) = mpsc::sync_channel::(1); + + std::thread::spawn(move || -> color_eyre::Result<()> { + let mut stdin_writer = BufWriter::new(stdin); + let mut first_line: Option = None; + let stdout_reader = BufReader::new(stdout); + + let mut wait_for_action = move || -> color_eyre::Result<()> { + debug!("waiting for action"); + match action_rx.recv() { + Ok(Action::Scan) => { + info!("scanning card"); + debug!("stdin: ('\\n\\n')"); + stdin_writer + .write_all(b"\n\n") + .expect("unable to write to stdin"); + stdin_writer.flush().expect("unable to flush stdin"); + Ok(()) + } + Ok(Action::Stop) => { + info!("stop received, exiting stdout process loop"); + Err(eyre!("stop received")) + } + Err(err) => { + error!("error receiving action: {}", err); + Err(eyre!("error receiving action")) + } + } + }; + + // don't start reading stdout until we've received the first action. + // we don't want to start scanning cards until we've received the first + // request to scan + wait_for_action()?; + + for line in stdout_reader.lines().flatten() { + debug!("[scanimage] stdout: {}", line); + if let Some(first_line) = first_line.take() { + card_tx + .send((first_line, line)) + .expect("unable to send card"); + // wait for the next scan action before reading the next card, + // just in case the caller wants to stop the scan session early + wait_for_action()?; + } else { + first_line = Some(line); + } + } + + if let Some(first_line) = first_line { + error!( + "[scanimage] unexpected end of stdout, dropping card: {}", + first_line + ); + } + + debug!("[scanimage] stdout closed"); + info!("scanimage session complete"); + Ok(()) + }); + + (action_tx, card_rx) +} + +impl ScanSession { + /// Start a new scan session. + fn start(directory: PathBuf) -> io::Result { + let batch_template = directory.join("scan-%04d.jpeg"); + let batch_template = batch_template + .to_str() + .expect("unable to convert path to string"); + let mut scanimage = start_scanimage(batch_template)?; + + let stdin = scanimage.stdin.take().ok_or_else(|| { + io::Error::new(io::ErrorKind::BrokenPipe, "scanimage stdin not available") + })?; + let stdout = scanimage.stdout.take().ok_or_else(|| { + io::Error::new(io::ErrorKind::BrokenPipe, "scanimage stdout not available") + })?; + let stderr = scanimage.stderr.take().ok_or_else(|| { + io::Error::new(io::ErrorKind::BrokenPipe, "scanimage stderr not available") + })?; + + let stderr_reader = BufReader::new(stderr); + let mut lines = stderr_reader.lines(); + + block_until_scanimage_ready(lines.by_ref())?; + pipe_stderr_to_log(lines); + let (action_tx, card_rx) = establish_batch_scanimage_duplex_channel(stdin, stdout); + + Ok(ScanSession { + directory, + scanimage, + action_tx, + card_rx, + }) + } + + /// Stop the scan session early, i.e. maybe before all pending cards have + /// been scanned. This method does not need to be called if the scan session + /// is iterated to completion. + pub fn stop(&mut self) -> io::Result<()> { + debug!("ScanSession::stop() called, sending Action::Stop"); + let _ = self.action_tx.send(Action::Stop); + self.scanimage.kill()?; + self.scanimage.wait()?; + Ok(()) + } +} + +impl Iterator for ScanSession { + type Item = Card; + + /// Scan the next card. Blocks until the card has been scanned. If `None` is + /// returned, the scan session has been stopped and `scanimage` has exited. + /// If `Some` is returned, the card has been scanned and the tuple contains + /// the paths to the front and back of the card. The paths will be files + /// inside the directory specified when the scan session was started. + fn next(&mut self) -> Option { + debug!("Iterator::next() called, sending Action::Scan"); + + if let Err(err) = self.action_tx.send(Action::Scan) { + debug!("action_tx.send() returned Err: {}", err); + return None; + } + + if let Ok((side_a_path, side_b_path)) = self.card_rx.recv() { + debug!( + "card_rx.recv() returned Ok: {:?}", + (&side_a_path, &side_b_path) + ); + let side_a_path = PathBuf::from(side_a_path); + let side_b_path = PathBuf::from(side_b_path); + assert!(side_a_path.starts_with(&self.directory)); + assert!(side_b_path.starts_with(&self.directory)); + Some((side_a_path, side_b_path)) + } else { + debug!("card_rx.recv() returned Err, scan session stopped"); + None + } + } +} + +impl Drop for ScanSession { + fn drop(&mut self) { + // make a best effort to stop the scan session + let _ = self.stop(); + } +} diff --git a/libs/central-scanner/src/lib.rs b/libs/central-scanner/src/lib.rs new file mode 100644 index 0000000000..ee6d10bf88 --- /dev/null +++ b/libs/central-scanner/src/lib.rs @@ -0,0 +1,3 @@ +mod fujitsu; + +pub use fujitsu::scan; diff --git a/libs/db/src/client.ts b/libs/db/src/client.ts index 82ba5c07d0..e5fb9c0486 100644 --- a/libs/db/src/client.ts +++ b/libs/db/src/client.ts @@ -8,6 +8,7 @@ import Database = require('better-sqlite3'); type Database = Database.Database; const debug = makeDebug('db-client'); +const queryDebug = debug.extend('query'); const MEMORY_DB_PATH = ':memory:'; @@ -182,6 +183,7 @@ export class Client { * client.run('insert into muppets (name) values (?)', 'Kermit') */ run

(sql: string, ...params: P): void { + queryDebug('run %s (%o)', sql, params); const db = this.getDatabase(); const stmt = db.prepare

(sql); stmt.run(...params); @@ -200,6 +202,7 @@ export class Client { * `) */ exec(sql: string): void { + queryDebug('exec %s', sql); const db = this.getDatabase(); db.exec(sql); } @@ -212,6 +215,7 @@ export class Client { * client.all('select * from muppets') */ all

(sql: string, ...params: P): unknown[] { + queryDebug('all %s (%o)', sql, params); const db = this.getDatabase(); const stmt = db.prepare

(sql); return stmt.all(...params); @@ -224,6 +228,7 @@ export class Client { sql: string, ...params: P ): IterableIterator { + queryDebug('each %s (%o)', sql, params); const db = this.getDatabase(); const stmt = db.prepare

(sql); return stmt.iterate(...params); @@ -237,6 +242,7 @@ export class Client { * client.one('select count(*) as count from muppets') */ one

(sql: string, ...params: P): unknown { + queryDebug('one %s (%o)', sql, params); const db = this.getDatabase(); const stmt = db.prepare

(sql); return stmt.get(...params); diff --git a/libs/monorepo-utils/bin/generate-circleci-config b/libs/monorepo-utils/bin/generate-circleci-config index b2a5d4e339..9b98ca8719 100755 --- a/libs/monorepo-utils/bin/generate-circleci-config +++ b/libs/monorepo-utils/bin/generate-circleci-config @@ -5,7 +5,7 @@ require('esbuild-runner/register'); const { join } = require('path'); -const { generateConfig } = require('../src/circleci'); +const { generateConfig, CIRCLECI_CONFIG_PATH } = require('../src/circleci'); const { getWorkspacePackageInfo } = require('../src/pnpm'); const { getRustPackageIds } = require('../src/cargo'); const { writeFileSync } = require('fs'); diff --git a/libs/monorepo-utils/package.json b/libs/monorepo-utils/package.json index e668b58a50..6ba3a7019a 100644 --- a/libs/monorepo-utils/package.json +++ b/libs/monorepo-utils/package.json @@ -40,7 +40,7 @@ "@typescript-eslint/eslint-plugin": "6.7.0", "@typescript-eslint/parser": "6.7.0", "esbuild-runner": "2.2.2", - "eslint": "8.49.0", + "eslint": "8.51.0", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-node": "^0.3.9", "eslint-import-resolver-typescript": "3.6.0", @@ -55,8 +55,7 @@ "lint-staged": "11.0.0", "prettier": "3.0.3", "sort-package-json": "^1.50.0", - "ts-jest": "29.1.1", - "typescript": "5.2.2" + "ts-jest": "29.1.1" }, "packageManager": "pnpm@8.1.0" } diff --git a/libs/monorepo-utils/src/circleci.ts b/libs/monorepo-utils/src/circleci.ts index bad8b88862..164ba0c17a 100644 --- a/libs/monorepo-utils/src/circleci.ts +++ b/libs/monorepo-utils/src/circleci.ts @@ -22,6 +22,7 @@ function generateTestJobForNodeJsPackage( const hasPlaywrightTests = existsSync(`${pkg.path}/playwright.config.ts`); const isIntegrationTestJob = hasPlaywrightTests; + /* istanbul ignore next */ const lines = [ `# ${pkg.name}`, `${jobIdForPackage(pkg)}:`, @@ -56,6 +57,7 @@ function generateTestJobForNodeJsPackage( ` path: ${pkg.relativePath}/reports/`, ]; + /* istanbul ignore next */ if (hasPlaywrightTests) { lines.push( ` - store_artifacts:`, diff --git a/libs/monorepo-utils/src/vscode.ts b/libs/monorepo-utils/src/vscode.ts index 1896351646..1c689fd93d 100644 --- a/libs/monorepo-utils/src/vscode.ts +++ b/libs/monorepo-utils/src/vscode.ts @@ -3,7 +3,8 @@ import { Result, err, iter, ok } from '@votingworks/basics'; import { readFileSync, writeFileSync } from 'fs'; import { dirname } from 'path'; -import { PackageInfo, getWorkspacePackageInfo } from './pnpm'; +import { getWorkspacePackageInfo } from './pnpm'; +import { PnpmPackageInfo } from '.'; /** * The contents of a VS Code `.code-workspace` file. @@ -29,7 +30,7 @@ const DEFAULT_EXTRA_FOLDERS: readonly WorkspaceFolder[] = [ 'libs/usb-mocking', ].map((path) => ({ path, name: path })); -function DEFAULT_PACKAGE_FILTER(pkg: PackageInfo) { +function DEFAULT_PACKAGE_FILTER(pkg: PnpmPackageInfo) { return !pkg.name.startsWith('@types/') && !pkg.name.endsWith('prodserver'); } @@ -53,7 +54,7 @@ export function updateWorkspaceConfig( check = false, }: { extraFolders?: readonly WorkspaceFolder[]; - pkgFilter?: (pkg: PackageInfo) => boolean; + pkgFilter?: (pkg: PnpmPackageInfo) => boolean; check?: boolean; } = {} ): Result { diff --git a/libs/test-utils/src/auth.ts b/libs/test-utils/src/auth.ts index 2a734d2516..e67ea202f9 100644 --- a/libs/test-utils/src/auth.ts +++ b/libs/test-utils/src/auth.ts @@ -4,6 +4,7 @@ import { DEFAULT_OVERALL_SESSION_TIME_LIMIT_HOURS, ElectionManagerUser, PollWorkerUser, + RaveVoterUser, SystemAdministratorUser, TEST_JURISDICTION, } from '@votingworks/types'; @@ -51,6 +52,18 @@ export function fakeCardlessVoterUser( }; } +export function fakeRaveVoterUser( + props: Partial = {} +): RaveVoterUser { + return { + role: 'rave_voter', + commonAccessCardId: 'test-common-access-card-id', + givenName: 'Bob', + familyName: 'Smith', + ...props, + }; +} + export function fakeSessionExpiresAt(): Date { return DateTime.now() .plus({ hours: DEFAULT_OVERALL_SESSION_TIME_LIMIT_HOURS }) diff --git a/libs/types-rs/Cargo.toml b/libs/types-rs/Cargo.toml new file mode 100644 index 0000000000..36f301bade --- /dev/null +++ b/libs/types-rs/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "types-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = { workspace = true } +base64-serde = { workspace = true } +color-eyre = { workspace = true } +hex = { workspace = true } +hmac-sha256 = { workspace = true } +lazy_static = { workspace = true } +nanoid = { workspace = true } +regex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true, optional = true } +time = { workspace = true } +uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +proptest = { workspace = true } + +[features] +backend = ["sqlx"] +sqlx = ["dep:sqlx"] diff --git a/libs/types-rs/package.json b/libs/types-rs/package.json new file mode 100644 index 0000000000..825cd2801f --- /dev/null +++ b/libs/types-rs/package.json @@ -0,0 +1,16 @@ +{ + "name": "@votingworks/types-rs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "cargo build", + "lint": "cargo clippy -- -D warnings", + "start": "cargo watch -x run", + "test": "cargo test" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "packageManager": "pnpm@8.1.0" +} diff --git a/libs/types-rs/src/ballot_card.rs b/libs/types-rs/src/ballot_card.rs new file mode 100644 index 0000000000..b00910d656 --- /dev/null +++ b/libs/types-rs/src/ballot_card.rs @@ -0,0 +1,194 @@ +use std::{fmt::Debug, hash::Hash}; + +use serde::{Deserialize, Serialize}; + +use crate::geometry::{GridUnit, Inch, PixelPosition, PixelUnit, Rect, Size, SubPixelUnit}; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub enum BallotPaperSize { + #[serde(rename = "letter")] + Letter, + #[serde(rename = "legal")] + Legal, +} + +/// Ballot card orientation. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub enum Orientation { + /// The ballot card is portrait and right-side up. + #[serde(rename = "portrait")] + Portrait, + + /// The ballot card is portrait and upside down. + #[serde(rename = "portrait-reversed")] + PortraitReversed, +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Geometry { + pub ballot_paper_size: BallotPaperSize, + pub pixels_per_inch: PixelUnit, + pub canvas_size: Size, + pub content_area: Rect, + pub timing_mark_size: Size, + pub grid_size: Size, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub enum BallotSide { + #[serde(rename = "front")] + Front, + #[serde(rename = "back")] + Back, +} + +/// Expected PPI for scanned ballot cards. +const SCAN_PIXELS_PER_INCH: PixelUnit = 200; + +/// Expected PPI for ballot card templates. +const TEMPLATE_PIXELS_PER_INCH: PixelUnit = 72; + +/// Template margins for the front and back of the ballot card in inches. +const BALLOT_CARD_TEMPLATE_MARGINS: Size = Size { + width: 0.5, + height: 0.5, +}; + +/// Scanned margins for the front and back of the ballot card in inches. +const BALLOT_CARD_SCAN_MARGINS: Size = Size { + width: 0.0, + height: 0.0, +}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct PaperInfo { + pub size: BallotPaperSize, + pub margins: Size, + pub pixels_per_inch: PixelUnit, +} + +impl PaperInfo { + /// Returns info for a letter-sized scanned ballot card. + pub const fn scanned_letter() -> Self { + Self { + size: BallotPaperSize::Letter, + margins: BALLOT_CARD_SCAN_MARGINS, + pixels_per_inch: SCAN_PIXELS_PER_INCH, + } + } + + /// Returns info for a legal-sized scanned ballot card. + pub const fn scanned_legal() -> Self { + Self { + size: BallotPaperSize::Legal, + margins: BALLOT_CARD_SCAN_MARGINS, + pixels_per_inch: SCAN_PIXELS_PER_INCH, + } + } + + /// Returns info for a letter-sized ballot card template. + pub const fn template_letter() -> Self { + Self { + size: BallotPaperSize::Letter, + margins: BALLOT_CARD_TEMPLATE_MARGINS, + pixels_per_inch: TEMPLATE_PIXELS_PER_INCH, + } + } + + /// Returns info for a legal-sized ballot card template. + pub const fn template_legal() -> Self { + Self { + size: BallotPaperSize::Legal, + margins: BALLOT_CARD_TEMPLATE_MARGINS, + pixels_per_inch: TEMPLATE_PIXELS_PER_INCH, + } + } + + /// Returns info for all supported scanned paper sizes. + pub const fn scanned() -> [Self; 2] { + [Self::scanned_letter(), Self::scanned_legal()] + } + + /// Returns info for all supported template paper sizes. + pub const fn template() -> [Self; 2] { + [Self::template_letter(), Self::template_legal()] + } + + pub fn compute_geometry(&self) -> Geometry { + let ballot_paper_size = self.size; + let margins = self.margins; + let pixels_per_inch = self.pixels_per_inch; + let (width, height) = match ballot_paper_size { + BallotPaperSize::Letter => (8.5 as Inch, 11.0 as Inch), + BallotPaperSize::Legal => (8.5 as Inch, 14.0 as Inch), + }; + let canvas_size = Size { + width: (pixels_per_inch as SubPixelUnit * (margins.width.mul_add(2.0, width))).round() + as PixelUnit, + height: (pixels_per_inch as SubPixelUnit * (margins.height.mul_add(2.0, height))) + .round() as PixelUnit, + }; + let content_area = Rect::new( + (pixels_per_inch as SubPixelUnit * margins.width).round() as PixelPosition, + (pixels_per_inch as SubPixelUnit * margins.height).round() as PixelPosition, + canvas_size.width + - (pixels_per_inch as SubPixelUnit * margins.width).round() as PixelUnit, + canvas_size.height + - (pixels_per_inch as SubPixelUnit * margins.height).round() as PixelUnit, + ); + let timing_mark_size = Size { + width: (3.0 / 16.0) * pixels_per_inch as SubPixelUnit, + height: (1.0 / 16.0) * pixels_per_inch as SubPixelUnit, + }; + let grid_size = match ballot_paper_size { + BallotPaperSize::Letter => Size { + width: 34, + height: 41, + }, + BallotPaperSize::Legal => Size { + width: 34, + height: 53, + }, + }; + + Geometry { + ballot_paper_size, + pixels_per_inch, + canvas_size, + content_area, + timing_mark_size, + grid_size, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ballot_side_deserialize() { + assert_eq!( + serde_json::from_str::(r#""front""#).unwrap(), + BallotSide::Front + ); + assert_eq!( + serde_json::from_str::(r#""back""#).unwrap(), + BallotSide::Back + ); + assert!(serde_json::from_str::(r#""foo""#).is_err()); + } + + #[test] + fn test_ballot_side_serialize() { + assert_eq!( + serde_json::to_string(&BallotSide::Front).unwrap(), + r#""front""# + ); + assert_eq!( + serde_json::to_string(&BallotSide::Back).unwrap(), + r#""back""# + ); + } +} diff --git a/libs/types-rs/src/cdf/cvr.rs b/libs/types-rs/src/cdf/cvr.rs new file mode 100644 index 0000000000..9d4900ae45 --- /dev/null +++ b/libs/types-rs/src/cdf/cvr.rs @@ -0,0 +1,1570 @@ +use lazy_static::lazy_static; +use nanoid::nanoid; +use regex::Regex; + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +/// A proper fractional value, represented using fractional or decimal notation. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct FractionalNumber(String); + +const FRACTIONAL_NUMBER_PATTERN: &str = "([0-9]+/[1-9]+[0-9]*)|(\\.[0-9]+)"; +lazy_static! { + static ref FRACTIONAL_NUMBER_REGEX: Regex = Regex::new(FRACTIONAL_NUMBER_PATTERN).unwrap(); +} + +impl std::str::FromStr for FractionalNumber { + type Err = String; + fn from_str(s: &str) -> Result { + if FRACTIONAL_NUMBER_REGEX.is_match(s) { + Ok(Self(s.to_string())) + } else { + Err(format!( + "{} does not match {}", + s, FRACTIONAL_NUMBER_PATTERN + )) + } + } +} + +/// Used in `SelectionPosition::IsAllocable` to indicate whether the +/// `SelectionPosition::NumberVotes` should be allocated to the underlying +/// contest option counter. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum AllocationStatus { + /// To not allocate votes to the contest option's accumulator. + #[serde(rename = "no")] + No, + + /// When the decision to allocate votes is unknown, such as when the adjudication is needed. + #[serde(rename = "unknown")] + Unknown, + + /// To allocate votes to the contest option's accumulator. + #[serde(rename = "yes")] + Yes, +} + +/// Used in CVRSnapshot::Status to identify the status of the CVR. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum CVRStatus { + /// To indicate that the CVR needs to be adjudicated. + #[serde(rename = "needs-adjudication")] + NeedsAdjudication, + + /// Used in conjunction with `CVRSnapshot::OtherStatus` when no other value + /// in this enumeration applies. + #[serde(rename = "other")] + Other, +} + +/// Used in `CVRSnapshot::Type` to indicate the type of snapshot. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CVRType { + /// Has been adjudicated. + #[serde(rename = "interpreted")] + Interpreted, + + /// After contest rules applied. + #[serde(rename = "modified")] + Modified, + + /// As scanned, no contest rules applied. + #[serde(rename = "original")] + #[default] + Original, +} + +/// To identify the version of the CVR specification being used, i.e., version +/// 1.0.0. This will need to be updated for different versions of the +/// specification. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum CastVoteRecordVersion { + /// Fixed value for the version of this specification. + #[serde(rename = "1.0.0")] + V1_0_0, +} + +/// Used in `CVRContestSelection::Status` to identify the status of a contest +/// selection in the CVR. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum ContestSelectionStatus { + /// To indicate that the contest selection was generated per contest rules. + #[serde(rename = "generated-rules")] + GeneratedRules, + + /// To indicate that the contest selection was invalidated by the generating device because of contest rules. + #[serde(rename = "invalidated-rules")] + InvalidatedRules, + + /// To indicate that the contest selection was flagged by the generating device for adjudication. + #[serde(rename = "needs-adjudication")] + NeedsAdjudication, + + /// Used in conjunction with `CVRContestSelection::OtherStatus` when no + /// other value in this enumeration applies. + #[serde(rename = "other")] + Other, +} + +/// Used in `CVRContest::Status` to identify the status of a contest in which +/// contest selection(s) were made. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum ContestStatus { + /// To indicate that the contest has been invalidated by the generating device because of contest rules. + #[serde(rename = "invalidated-rules")] + InvalidatedRules, + + /// For a `CVRContest` with no `SelectionPosition`, i.e. to specify the + /// position contains no marks or other indications. + #[serde(rename = "not-indicated")] + NotIndicated, + + /// Used in conjunction with `CVRContest::OtherStatus` when no other value + /// in this enumeration applies. + #[serde(rename = "other")] + Other, + + /// To indicate that the contest was overvoted. + #[serde(rename = "overvoted")] + Overvoted, + + /// To indicate that the contest was undervoted. + #[serde(rename = "undervoted")] + Undervoted, +} + +/// Used in `Hash::Type` to indicate the type of hash being used for an image +/// file. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum HashType { + /// To indicate that the MD6 message digest algorithm is being used. + #[serde(rename = "md6")] + Md6, + + /// Used in conjunction with Hash::OtherType when no other value in this + /// enumeration applies. + #[serde(rename = "other")] + Other, + + /// To indicate that the SHA 256-bit signature is being used. + #[serde(rename = "sha-256")] + Sha256, + + /// To indicate that the SHA 512-bit (32-byte) signature is being used. + #[serde(rename = "sha-512")] + Sha512, +} + +/// Used in `Code::Type` to indicate the type of code/identifier being used. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum IdentifierType { + /// To indicate that the identifier is a FIPS code. + #[serde(rename = "fips")] + Fips, + + /// To indicate that the identifier is from a local-level scheme, i.e., + /// unique to a county or city. + #[serde(rename = "local-level")] + LocalLevel, + + /// To indicate that the identifier is from a national-level scheme other + /// than FIPS or OCD-ID. + #[serde(rename = "national-level")] + NationalLevel, + + /// To indicate that the identifier is from the OCD-ID scheme. + #[serde(rename = "ocd-id")] + OcdId, + + /// Used in conjunction with Code::OtherType when no other value in this + /// enumeration applies. + #[serde(rename = "other")] + Other, + + /// To indicate that the identifier is from a state-level scheme, i.e., + /// unique to a particular state. + #[serde(rename = "state-level")] + StateLevel, +} + +/// Used in SelectionPosition::HasIndication to identify whether a selection +/// indication is present. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum IndicationStatus { + /// There is no selection indication. + #[serde(rename = "no")] + No, + + /// It is unknown whether there is a selection indication, e.g., used for ambiguous marks. + #[serde(rename = "unknown")] + #[default] + Unknown, + + /// There is a selection indication present. + #[serde(rename = "yes")] + Yes, +} + +/// Used in `SelectionPosition::Status` to identify the status of a selection +/// indication. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum PositionStatus { + /// Used if the indication was adjudicated. + #[serde(rename = "adjudicated")] + Adjudicated, + + /// Used if the indication was generated by the creating device per contest + /// rules. + #[serde(rename = "generated-rules")] + GeneratedRules, + + /// Used if the indication was invalidated by the creating device because of + /// contest rules. + #[serde(rename = "invalidated-rules")] + InvalidatedRules, + + /// Used in conjunction with `SelectionPosition::OtherStatus` when no other value in this enumeration applies. + #[serde(rename = "other")] + Other, +} + +/// Used in `CastVoteRecordReport::ReportType` to indicate the type of the CVR report. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum ReportType { + /// To indicate that the report contains adjudications. + #[serde(rename = "adjudicated")] + Adjudicated, + + /// To indicate that the report is an aggregation of device reports. + #[serde(rename = "aggregated")] + Aggregated, + + /// To indicate that the report is an export from a device such as a + /// scanner. + #[serde(rename = "originating-device-export")] + OriginatingDeviceExport, + + /// Used in conjunction with `CastVoteRecordReport::OtherReportType` when no + /// other value in this enumeration applies. + #[serde(rename = "other")] + Other, + + /// To indicate that the report is the result of a ranked choice voting + /// round. + #[serde(rename = "rcv-round")] + RcvRound, +} + +/// Used in `GpUnit::Type` to indicate a type of political geography. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum ReportingUnitType { + /// To indicate a combined precinct. + #[serde(rename = "combined-precinct")] + CombinedPrecinct, + + /// Used in conjunction with `GpUnit::OtherType` when no other value in this + /// enumeration applies. + #[serde(rename = "other")] + Other, + + /// To indicate a polling place. + #[serde(rename = "polling-place")] + PollingPlace, + + /// To indicate a precinct. + #[serde(rename = "precinct")] + Precinct, + + /// To indicate a split-precinct. + #[serde(rename = "split-precinct")] + SplitPrecinct, + + /// To indicate a vote-center. + #[serde(rename = "vote-center")] + VoteCenter, +} + +/// Used in `Contest::VoteVariation` to indicate the vote variation (vote +/// method) used to tabulate the contest. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum VoteVariation { + /// To indicate approval voting. + #[serde(rename = "approval")] + Approval, + + /// To indicate the borda count method. + #[serde(rename = "borda")] + Borda, + + /// To indicate cumulative voting. + #[serde(rename = "cumulative")] + Cumulative, + + /// To indicate majority voting. + #[serde(rename = "majority")] + Majority, + + /// To indicate the N of M voting method. + #[serde(rename = "n-of-m")] + NOfM, + + /// Used in conjunction with `Contest::OtherVoteVariation` when no other + /// value in this enumeration applies. + #[serde(rename = "other")] + Other, + + /// To indicate plurality voting. + #[serde(rename = "plurality")] + Plurality, + + /// To indicate proportional voting. + #[serde(rename = "proportional")] + Proportional, + + /// To indicate range voting. + #[serde(rename = "range")] + Range, + + /// To indicate Ranked Choice Voting (RCV). + #[serde(rename = "rcv")] + Rcv, + + /// To indicate the super majority voting method. + #[serde(rename = "super-majority")] + SuperMajority, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum AnnotationObjectType { + #[serde(rename = "CVR.Annotation")] + #[default] + Annotation, +} + +/// Annotation is used to record annotations made by one or more adjudicators. +/// `CVRSnapshot` includes `Annotation`. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Annotation { + #[serde(rename = "@type")] + pub object_type: AnnotationObjectType, + + /// The name(s) of the adjudicator(s). + #[serde(rename = "AdjudicatorName", skip_serializing_if = "Option::is_none")] + pub adjudicator_name: Option>, + + /// A message created by the adjudicator(s). + #[serde(rename = "Message", skip_serializing_if = "Option::is_none")] + pub message: Option>, + + /// The date and time of the annotation. + #[serde( + rename = "TimeStamp", + with = "time::serde::iso8601::option", + skip_serializing_if = "Option::is_none" + )] + pub time_stamp: Option, +} + +/// `BallotMeasureContest` is a subclass of `Contest` and is used to identify +/// the type of contest as involving one or more ballot measures. It inherits +/// attributes from `Contest`. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct BallotMeasureContest { + #[serde(rename = "@id")] + pub id: String, + + /// An abbreviation associated with the contest. + #[serde(rename = "Abbreviation", skip_serializing_if = "Option::is_none")] + pub abbreviation: Option, + + /// A code or identifier used for this contest. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Identifies the contest selections in the contest. + #[serde(rename = "ContestSelection")] + pub contest_selection: Vec, + + /// Title or name of the contest, e.g., "Governor" or "Question on Legalization of Gambling". + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// If `VoteVariation` is 'other', the vote variation for this contest. + #[serde(rename = "OtherVoteVariation", skip_serializing_if = "Option::is_none")] + pub other_vote_variation: Option, + + /// The vote variation for this contest, from the `VoteVariation` + /// enumeration. + #[serde(rename = "VoteVariation", skip_serializing_if = "Option::is_none")] + pub vote_variation: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum BallotMeasureSelectionObjectType { + #[serde(rename = "CVR.BallotMeasureSelection")] + #[default] + BallotMeasure, +} + +/// `BallotMeasureSelection` is a subclass of `ContestSelection` and is used for +/// ballot measures. The voter's selected response to the contest selection +/// (e.g., "yes" or "no") may be in English or other languages as utilized on +/// the voter's ballot. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct BallotMeasureSelection { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: BallotMeasureSelectionObjectType, + + /// Code used to identify the contest selection. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// The voter's selection, i.e., 'yes' or 'no', in English or in other languages as utilized on the voter's ballot. + #[serde(rename = "Selection")] + pub selection: String, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CVRObjectType { + #[serde(rename = "CVR.CVR")] + #[default] + Cvr, +} + +/// CVR constitutes a cast vote record, generated by a ballot scanning device, containing indications of contests and contest options chosen by the voter, as well as other information for auditing and annotation purposes. Each sheet of a multi-page paper ballot is represented by an individual CVR, e.g., if all sheets of a 5-sheet ballot are scanned, 5 CVRs will be created. CastVoteRecordReport includes multiple instances of CVR as applicable. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Cvr { + #[serde(rename = "@type")] + pub object_type: CVRObjectType, + + /// A unique identifier for this CVR, used to link the CVR with the corresponding audit record, e.g., a paper ballot. This identifier may be impressed on the corresponding audit record as it is scanned, or otherwise associated with the corresponding ballot. + #[serde(rename = "BallotAuditId", skip_serializing_if = "Option::is_none")] + pub ballot_audit_id: Option, + + /// An image of the ballot sheet created by the scanning device. + #[serde(rename = "BallotImage", skip_serializing_if = "Option::is_none")] + pub ballot_image: Option>, + + /// A unique identifier for the ballot (or sheet of a multi-sheet ballot) that this CVR represents, used if ballots are pre-marked with unique identifiers. If provided, this number would be the same on all CVRs that represent individual sheets from the same multi-sheet ballot. This identifier is not the same as one that may be impressed on the corresponding ballot as it is scanned or otherwise associated with the corresponding ballot; see the BallotAuditId attribute. + #[serde(rename = "BallotPrePrintedId", skip_serializing_if = "Option::is_none")] + pub ballot_pre_printed_id: Option, + + /// A unique number for the ballot (or sheet of a multi-sheet ballot) that this CVR represents, used if ballots are pre-marked with unique numbers. If provided, this number would be the same on all CVRs that represent individual sheets from the same multi-sheet ballot. This number is not the same as one that may be impressed on the corresponding ballot as it is scanned or otherwise associated with the corresponding ballot; see the BallotAuditId attribute. + #[serde(rename = "BallotSheetId", skip_serializing_if = "Option::is_none")] + pub ballot_sheet_id: Option, + + /// An identifier of the ballot style associated with the corresponding ballot. + #[serde(rename = "BallotStyleId", skip_serializing_if = "Option::is_none")] + pub ballot_style_id: Option, + + /// Identifies the smallest unit of geography associated with the corresponding ballot, typically a precinct or split-precinct. + #[serde(rename = "BallotStyleUnitId", skip_serializing_if = "Option::is_none")] + pub ballot_style_unit_id: Option, + + /// The identifier for the batch that includes this CVR. + #[serde(rename = "BatchId", skip_serializing_if = "Option::is_none")] + pub batch_id: Option, + + /// The sequence number of the corresponding paper ballot within a batch. + #[serde(rename = "BatchSequenceId", skip_serializing_if = "Option::is_none")] + pub batch_sequence_id: Option, + + /// Identifies the repeatable portion of the CVR that links to contest selections and related information. + #[serde(rename = "CVRSnapshot")] + pub cvr_snapshot: Vec, + + /// Identifies the device that created the CVR. + #[serde(rename = "CreatingDeviceId", skip_serializing_if = "Option::is_none")] + pub creating_device_id: Option, + + /// Identifies the snapshot that is currently tabulatable. + #[serde(rename = "CurrentSnapshotId")] + pub current_snapshot_id: String, + + /// Used to identify an election with which the CVR is associated. + #[serde(rename = "ElectionId")] + pub election_id: String, + + /// Identifies the party associated with a CVR, typically for partisan primaries. + #[serde(rename = "PartyIds", skip_serializing_if = "Option::is_none")] + pub party_ids: Option>, + + /// The sequence number for this CVR. This represents the ordinal number that this CVR was processed by the tabulating device. + #[serde(rename = "UniqueId", skip_serializing_if = "Option::is_none")] + pub unique_id: Option, + + /// Indicates whether the ballot is an absentee or precinct ballot. + #[serde(rename = "vxBallotType")] + pub vx_ballot_type: VxBallotType, +} + +impl Default for Cvr { + fn default() -> Self { + Self { + object_type: CVRObjectType::Cvr, + ballot_audit_id: None, + ballot_image: None, + ballot_pre_printed_id: None, + ballot_sheet_id: None, + ballot_style_id: None, + ballot_style_unit_id: None, + batch_id: None, + batch_sequence_id: None, + cvr_snapshot: vec![], + creating_device_id: None, + current_snapshot_id: "".to_string(), + election_id: "".to_string(), + party_ids: None, + unique_id: None, + vx_ballot_type: VxBallotType::Precinct, + } + } +} + +/// Used in `CVR::vxBallotType` to indicate whether the ballot is an absentee or +/// precinct ballot. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub enum VxBallotType { + #[serde(rename = "precinct")] + Precinct, + + #[serde(rename = "absentee")] + Absentee, + + #[serde(rename = "provisional")] + Provisional, +} + +impl VxBallotType { + pub fn max() -> u32 { + // updating this value is a breaking change + 2u32.pow(4) - 1 + } +} + +impl From for u32 { + fn from(vx_ballot_type: VxBallotType) -> Self { + match vx_ballot_type { + VxBallotType::Precinct => 0, + VxBallotType::Absentee => 1, + VxBallotType::Provisional => 2, + } + } +} + +impl From for VxBallotType { + fn from(vx_ballot_type: u32) -> Self { + match vx_ballot_type { + 0 => VxBallotType::Precinct, + 1 => VxBallotType::Absentee, + 2 => VxBallotType::Provisional, + _ => panic!("Invalid VxBallotType"), + } + } +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CVRContestObjectType { + #[serde(rename = "CVR.CVRContest")] + #[default] + CVRContest, +} + +/// `CVRContest` class is included by `CVRSnapshot` for each contest on the +/// ballot that was voted, that is, whose contest options contain indications +/// that may constitute a vote. CVRContest includes CVRContestSelection for each +/// contest option in the contest containing an indication or write-in. +/// CVRSnapshot can also include CVRContest for every contest on the ballot +/// regardless of whether any of the contest options contain an indication, for +/// cases where the CVR must include all contests that appeared on the ballot. +/// CVRContest attributes are for including summary information about the +/// contest. Overvotes plus Undervotes plus TotalVotes must equal the number +/// of votes allowable in the contest, e.g., in a "chose 3 of 5" +/// contest in which the voter chooses only 2, then Overvotes = 0, Undervotes = +/// 1, and TotalVotes = 2, which adds up to the number of votes allowable = 3. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub struct CVRContest { + #[serde(rename = "@type")] + pub object_type: CVRContestObjectType, + + /// Used to include information about a contest selection in the contest, including the associated indication(s). + #[serde( + rename = "CVRContestSelection", + skip_serializing_if = "Option::is_none" + )] + pub cvr_contest_selection: Option>, + + /// Used to link to an instance of Contest specific to the contest at hand, for the purpose of specifying information about the contest such as its contest identifier. + #[serde(rename = "ContestId")] + pub contest_id: String, + + /// Used when Status is 'other' to include a user-defined status. + #[serde(rename = "OtherStatus", skip_serializing_if = "Option::is_none")] + pub other_status: Option, + + /// The number of votes lost due to overvoting. + #[serde(rename = "Overvotes", skip_serializing_if = "Option::is_none")] + pub overvotes: Option, + + /// Used to indicate the number of possible contest selections in the contest. + #[serde(rename = "Selections", skip_serializing_if = "Option::is_none")] + pub selections: Option, + + /// The status of the contest, e.g., overvoted, undervoted, from the + /// `ContestStatus` enumeration. If no values apply, use 'other' and + /// include a user-defined status in `OtherStatus.` + #[serde(rename = "Status", skip_serializing_if = "Option::is_none")] + pub status: Option>, + + /// The number of votes lost due to undervoting. + #[serde(rename = "Undervotes", skip_serializing_if = "Option::is_none")] + pub undervotes: Option, + + /// The total number of write-ins in the contest. + #[serde(rename = "WriteIns", skip_serializing_if = "Option::is_none")] + pub write_ins: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CVRContestSelectionObjectType { + #[serde(rename = "CVR.CVRContestSelection")] + #[default] + CVRContestSelection, +} + +/// `CVRContestSelection` is used to link a contest option containing an +/// indication with information about the indication, such as whether a mark +/// constitutes a countable vote, or whether a mark is determined to be +/// marginal, etc. `CVRContest` includes an instance of `CVRContestSelection` +/// when an indication for the selection is present, and `CVRContestSelection` +/// then includes `SelectionPosition` for each indication present. To tie the +/// indication to the specific contest selection, `CVRContestSelection` links to +/// an instance of `ContestSelection` that has previously been included by +/// `Contest.` Since multiple indications per contest option are possible for +/// some voting methods, `CVRContestSelection` can include multiple instances of +/// SelectionPosition, one per indication. `CVRContestSelection` can also be +/// used for the purpose of including, in the CVR, all contest options in the +/// contest regardless of whether indications are present. In this case, +/// `CVRContestSelection` would not include `SelectionPosition` if no indication +/// is present but would link to the appropriate instance of `ContestSelection.` +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub struct CVRContestSelection { + #[serde(rename = "@type")] + pub object_type: CVRContestSelectionObjectType, + + /// Used to link to an instance of a contest selection that was previously + /// included by Contest. + #[serde(rename = "ContestSelectionId", skip_serializing_if = "Option::is_none")] + pub contest_selection_id: Option, + + /// Used to include the ordinal position of the contest option as it + /// appeared on the ballot. + #[serde(rename = "OptionPosition", skip_serializing_if = "Option::is_none")] + pub option_position: Option, + + /// Used when Status is 'other' to include a user-defined status. + #[serde(rename = "OtherStatus", skip_serializing_if = "Option::is_none")] + pub other_status: Option, + + /// For the RCV voting variation, the rank chosen by the voter, for when a + /// contest selection can represent a ranking. + #[serde(rename = "Rank", skip_serializing_if = "Option::is_none")] + pub rank: Option, + + /// Used to include further information about the indication/mark associated + /// with the contest selection. Depending on the voting method, multiple + /// indications/marks per selection may be possible. + #[serde(rename = "SelectionPosition")] + pub selection_position: Vec, + + /// Contains the status of the contest selection, e.g., 'needs-adjudication' + /// for a contest requiring adjudication, using values from the + /// `ContestSelectionStatus` enumeration. If no values apply, use 'other' and + /// include a user-defined status in OtherStatus. + #[serde(rename = "Status", skip_serializing_if = "Option::is_none")] + pub status: Option>, + + /// For cumulative or range and other similar voting variations, contains + /// the total proper fractional number of votes across all + /// indications/marks. + #[serde( + rename = "TotalFractionalVotes", + skip_serializing_if = "Option::is_none" + )] + pub total_fractional_votes: Option, + + /// For cumulative or range and other similar voting variations, contains + /// the total number of votes across all indications/marks. + #[serde(rename = "TotalNumberVotes", skip_serializing_if = "Option::is_none")] + pub total_number_votes: Option, +} + +/// `CVRSnapshot` contains a version of the contest selections for a CVR; there +/// can be multiple versions of `CVRSnapshot` within the same CVR. Type +/// specifies the type of the snapshot, i.e., whether interpreted by the scanner +/// according to contest rules, modified as a result of adjudication, or the +/// original, that is, the version initially scanned before contest rules are +/// applied. CVR includes `CVRSnapshot.Other` attributes are repeated in each +/// `CVRSnapshot` because they may differ across snapshots, e.g., the contests +/// could be different as well as other status. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CVRSnapshotObjectType { + #[serde(rename = "CVR.CVRSnapshot")] + #[default] + CVRSnapshot, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct CVRSnapshot { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: CVRSnapshotObjectType, + + /// Used to include an annotation associated with the CVR snapshot. + #[serde(rename = "Annotation", skip_serializing_if = "Option::is_none")] + pub annotation: Option>, + + /// Identifies the contests in the CVR. + #[serde(rename = "CVRContest", skip_serializing_if = "Option::is_none")] + pub cvr_contest: Option>, + + /// When Status is 'other', contains the ballot status. + #[serde(rename = "OtherStatus", skip_serializing_if = "Option::is_none")] + pub other_status: Option, + + /// The status of the CVR. + #[serde(rename = "Status", skip_serializing_if = "Option::is_none")] + pub status: Option>, + + /// The type of the snapshot, e.g., original. + #[serde(rename = "Type")] + pub snapshot_type: CVRType, +} + +impl Default for CVRSnapshot { + fn default() -> Self { + Self { + id: nanoid!(), + object_type: CVRSnapshotObjectType::default(), + annotation: None, + cvr_contest: None, + other_status: None, + status: None, + snapshot_type: CVRType::default(), + } + } +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CVRWriteInObjectType { + #[serde(rename = "CVR.CVRWriteIn")] + #[default] + CVRWriteIn, +} + +/// `CVRWriteIn` is used when the contest selection is a write-in. It has +/// attributes for the image or text of the write-in. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub struct CVRWriteIn { + #[serde(rename = "@type")] + pub object_type: CVRWriteInObjectType, + + /// Used for the text of the write-in, typically present when the CVR has + /// been created by electronic ballot marking equipment. + #[serde(rename = "Text", skip_serializing_if = "Option::is_none")] + pub text: Option, + + /// Used for an image of the write-in, typically made by a scanner when + /// scanning a paper ballot. + #[serde(rename = "WriteInImage", skip_serializing_if = "Option::is_none")] + pub write_in_image: Option, +} + +/// Candidate identifies a candidate in a contest on the voter's ballot. +/// `Election` includes instances of `Candidate` for each candidate in a contest; +/// typically, only those candidates who received votes would be included. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CandidateObjectType { + #[serde(rename = "CVR.Candidate")] + #[default] + Candidate, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Candidate { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: CandidateObjectType, + + /// A code or identifier associated with the candidate. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Candidate's name as listed on the ballot. + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The party associated with the candidate. + #[serde(rename = "PartyId", skip_serializing_if = "Option::is_none")] + pub party_id: Option, +} + +/// `CandidateContest` is a subclass of `Contest` and is used to identify the type +/// of contest as involving one or more candidates. It inherits attributes from +/// `Contest.` +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CandidateContestObjectType { + #[serde(rename = "CVR.CandidateContest")] + #[default] + CandidateContest, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct CandidateContest { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: CandidateContestObjectType, + + /// An abbreviation associated with the contest. + #[serde(rename = "Abbreviation", skip_serializing_if = "Option::is_none")] + pub abbreviation: Option, + + /// A code or identifier used for this contest. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Identifies the contest selections in the contest. + #[serde(rename = "ContestSelection")] + pub contest_selection: Vec, + + /// Title or name of the contest, e.g., "Governor" or "Question on Legalization of Gambling". + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The number of candidates to be elected in the contest. + #[serde(rename = "NumberElected", skip_serializing_if = "Option::is_none")] + pub number_elected: Option, + + /// If VoteVariation is 'other', the vote variation for this contest. + #[serde(rename = "OtherVoteVariation", skip_serializing_if = "Option::is_none")] + pub other_vote_variation: Option, + + /// The party associated with the contest, if a partisan primary. + #[serde(rename = "PrimaryPartyId", skip_serializing_if = "Option::is_none")] + pub primary_party_id: Option, + + /// The vote variation for this contest, from the VoteVariation enumeration. + #[serde(rename = "VoteVariation", skip_serializing_if = "Option::is_none")] + pub vote_variation: Option, + + /// The number of votes allowed in the contest, e.g., 3 for a 'choose 3 of 5 candidates' contest. + #[serde(rename = "VotesAllowed", skip_serializing_if = "Option::is_none")] + pub votes_allowed: Option, +} + +/// `CandidateSelection` is a subclass of `ContestSelection` and is used for +/// candidates, including for write-in candidates. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CandidateSelectionObjectType { + #[serde(rename = "CVR.CandidateSelection")] + #[default] + CandidateSelection, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct CandidateSelection { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: CandidateSelectionObjectType, + + /// The candidate associated with the contest selection. For contests involving a ticket of multiple candidates, an ordered list of candidates as they appeared on the ballot would be created. + #[serde(rename = "CandidateIds", skip_serializing_if = "Option::is_none")] + pub candidate_ids: Option>, + + /// Code used to identify the contest selection. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// A flag to indicate if the candidate selection is associated with a write-in. + #[serde(rename = "IsWriteIn", skip_serializing_if = "Option::is_none")] + pub is_write_in: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CastVoteRecordReportObjectType { + #[serde(rename = "CVR.CastVoteRecordReport")] + #[default] + CastVoteRecordReport, +} + +/// The root class/element; attributes pertain to the status and format of the report and when created. CastVoteRecordReport includes multiple instances of CVR, one per CVR or sheet of a multi-page cast vote record. CastVoteRecordReport also includes multiple instances of Contest, typically only for those contests that were voted so as to reduce file size. The Contest instances are later referenced by other classes to link them to contest options that were voted and the indication(s)/mark(s) made. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct CastVoteRecordReport { + #[serde(rename = "@type")] + pub object_type: CastVoteRecordReportObjectType, + + /// Used to include instances of CVR classes, one per cast vote record in the report. + #[serde(rename = "CVR", skip_serializing_if = "Option::is_none")] + pub cvr: Option>, + + /// Used to include the election(s) associated with the CVRs. + #[serde(rename = "Election")] + pub election: Vec, + + /// Identifies the time that the election report was created. + #[serde(rename = "GeneratedDate", with = "time::serde::iso8601")] + pub generated_date: OffsetDateTime, + + /// Used to include the political geography, i.e., location, for where the cast vote record report was created and for linking cast vote records to their corresponding precinct or split (or otherwise smallest unit). + #[serde(rename = "GpUnit")] + pub gp_unit: Vec, + + /// Notes that can be added as appropriate, presumably by an adjudicator. + #[serde(rename = "Notes", skip_serializing_if = "Option::is_none")] + pub notes: Option, + + /// If ReportType is 'other', this contains the report type. + #[serde(rename = "OtherReportType", skip_serializing_if = "Option::is_none")] + pub other_report_type: Option, + + /// The party associated with the ballot sheet for a partisan primary. + #[serde(rename = "Party", skip_serializing_if = "Option::is_none")] + pub party: Option>, + + /// Identifies the device used to create the CVR report. + #[serde(rename = "ReportGeneratingDeviceIds")] + pub report_generating_device_ids: Vec, + + /// The type of report, using the ReportType enumeration. + #[serde(rename = "ReportType", skip_serializing_if = "Option::is_none")] + pub report_type: Option>, + + /// The device creating the report. The reporting device need not necessarily be the creating device, i.e., for an aggregated report, the reporting device could be an EMS used to aggregate and tabulate cast vote records. + #[serde(rename = "ReportingDevice")] + pub reporting_device: Vec, + + /// The version of the CVR specification being used (1.0). + #[serde(rename = "Version")] + pub version: CastVoteRecordVersion, + + /// List of scanner batches with metadata. + #[serde(rename = "vxBatch")] + vx_batch: Vec, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum VxBatchObjectType { + #[serde(rename = "CVR.vxBatch")] + #[default] + VxBatch, +} + +/// Entity containing metadata about a scanned batch. Cast vote records link to batches via CVR::BatchId. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct VxBatch { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: VxBatchObjectType, + + /// A human readable label for the batch. + #[serde(rename = "BatchLabel")] + batch_label: String, + + /// The ordinal number of the batch in the tabulator's sequence of batches in a given election. + #[serde(rename = "SequenceId")] + sequence_id: u64, + + /// The start time of the batch. On a precinct scanner, the start time is when the polls are opened or voting is resumed. On a central scanner, the start time is when the user initiates scanning a batch. + #[serde(rename = "StartTime", with = "time::serde::iso8601")] + start_time: OffsetDateTime, + + /// The end time of the batch. On a precinct scanner, the end time is when the polls are closed or voting is paused. On a central scanner, the end time is when a batch scan is complete + #[serde( + rename = "EndTime", + default, + skip_serializing_if = "Option::is_none", + with = "time::serde::iso8601::option" + )] + end_time: Option, + + /// The number of sheets included in a batch. + #[serde(rename = "NumberSheets")] + number_sheets: u64, + + /// The tabulator that created the batch. + #[serde(rename = "CreatingDeviceId")] + creating_device_id: String, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum CodeObjectType { + #[serde(rename = "CVR.Code")] + #[default] + Code, +} + +/// Code is used in Election, GpUnit, Contest, Candidate, and Party to identify an associated code as well as the type of code. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Code { + #[serde(rename = "@type")] + pub object_type: CodeObjectType, + + /// A label associated with the code, used as needed. + #[serde(rename = "Label", skip_serializing_if = "Option::is_none")] + pub label: Option, + + /// If Type is 'other', the type of code. + #[serde(rename = "OtherType", skip_serializing_if = "Option::is_none")] + pub other_type: Option, + + /// Used to indicate the type of code, from the IdentifierType enumeration. + #[serde(rename = "Type")] + pub code_type: IdentifierType, + + /// The value of the code, i.e., the identifier. + #[serde(rename = "Value")] + pub value: String, +} + +/// Contest represents a contest on the ballot. CastVoteRecordReport initially includes an instance of Contest for each contest on the ballot. Other classes can subsequently reference the instances as necessary to link together items on the cast vote record, such as a contest, its voted contest selection(s), and the mark(s) associated with the selection(s). +/// +/// Contest has three subclasses, each used for a specific type of contest: These subclasses inherit Contest's attributes. +/// +/// PartyContest - used for straight party contests, +/// +/// BallotMeasureContest - used for contests, and +/// +/// CandidateContest - used for candidate contests. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Contest { + #[serde(rename = "@id")] + pub id: String, + + /// An abbreviation associated with the contest. + #[serde(rename = "Abbreviation", skip_serializing_if = "Option::is_none")] + pub abbreviation: Option, + + /// A code or identifier used for this contest. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Identifies the contest selections in the contest. + #[serde(rename = "ContestSelection")] + pub contest_selection: Vec, + + /// Title or name of the contest, e.g., "Governor" or "Question on Legalization of Gambling". + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// If VoteVariation is 'other', the vote variation for this contest. + #[serde(rename = "OtherVoteVariation", skip_serializing_if = "Option::is_none")] + pub other_vote_variation: Option, + + /// The vote variation for this contest, from the VoteVariation enumeration. + #[serde(rename = "VoteVariation", skip_serializing_if = "Option::is_none")] + pub vote_variation: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +#[serde(tag = "@type")] +pub enum AnyContestSelection { + #[serde(rename = "CVR.ContestSelection")] + Contest(ContestSelection), + #[serde(rename = "CVR.PartySelection")] + Party(PartySelection), + #[serde(rename = "CVR.BallotMeasureSelection")] + BallotMeasure(BallotMeasureSelection), + #[serde(rename = "CVR.CandidateSelection")] + Candidate(CandidateSelection), +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +#[serde(tag = "@type")] +pub enum AnyContest { + #[serde(rename = "CVR.Contest")] + Contest(Contest), + #[serde(rename = "CVR.PartyContest")] + PartyContest(PartyContest), + #[serde(rename = "CVR.BallotMeasureContest")] + BallotMeasureContest(BallotMeasureContest), + #[serde(rename = "CVR.CandidateContest")] + CandidateContest(CandidateContest), +} + +/// ContestSelection represents a contest selection in a contest. Contest can include an instance of ContestSelection for each contest selection in the contest or, as desired, all contest selections. +/// +/// ContestSelection has three subclasses, each used for a specific type of contest selection: +/// +/// BallotMeasureSelection - used for ballot measures, +/// +/// CandidateSelection - used for candidate selections, and +/// +/// PartySelection - used for straight party selections. +/// +/// Instances of CVRContestSelection subsequently link to the contest selections as needed so as to tie together the contest, the contest selection, and the mark(s) made for the contest selection. +/// +/// ContestSelection contains one attribute, Code, that can be used to identify the contest selection and thereby eliminate the need to identify it using the subclasses. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum ContestSelectionObjectType { + #[serde(rename = "CVR.ContestSelection")] + #[default] + ContestSelection, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct ContestSelection { + #[serde(rename = "@id")] + pub id: String, + + // #[serde(rename = "@type")] + // pub object_type: ContestSelectionObjectType, + /// Code used to identify the contest selection. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, +} + +/// Election defines instances of the Contest and Candidate classes so that they can be later referenced in CVR classes. Election includes an instance of Contest for each contest in the election and includes an instance of Candidate for each candidate. This is done to utilize file sizes more efficiently; otherwise each CVR would need to define these instances separately and much duplication would occur. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum ElectionObjectType { + #[serde(rename = "CVR.Election")] + #[default] + Election, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Election { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: ElectionObjectType, + + /// Used to establish a collection of candidate definitions that will be referenced by the CVRs. The contests in each CVR will reference the candidate definitions. + #[serde(rename = "Candidate", skip_serializing_if = "Option::is_none")] + pub candidate: Option>, + + /// Used for a code associated with the election, e.g., a precinct identifier if the election scope is a precinct. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Used for establishing a collection of contest definitions that will be referenced by the CVRs. + #[serde(rename = "Contest")] + pub contest: Vec, + + /// Used to identify the election scope, i.e., the political geography corresponding to the election. + #[serde(rename = "ElectionScopeId")] + pub election_scope_id: String, + + /// A text string identifying the election. + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum FileObjectType { + #[serde(rename = "CVR.File")] + #[default] + File, +} + +/// Used to hold the contents of a file or identify a file created by the scanning device. The file generally would contain an image of the scanned ballot or an image of a write-in entered by a voter onto the scanned ballot. SubClass Image is used if the file contains an image. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct File { + #[serde(rename = "@type")] + pub object_type: FileObjectType, + + #[serde(rename = "Data")] + pub data: String, + + /// Contains the name of the file or an identifier of the file. + #[serde(rename = "FileName", skip_serializing_if = "Option::is_none")] + pub file_name: Option, + + /// The mime type of the file, e.g., image/jpeg. + #[serde(rename = "MimeType", skip_serializing_if = "Option::is_none")] + pub mime_type: Option, +} + +/// Used for identifying a geographical unit for various purposes, including: +/// +/// The reporting unit of the report generation device, e.g., a precinct location of a scanner that generates the collection of CVRs, +/// +/// The geographical scope of the election, or the unit of geography associated with an individual CVR. +/// +/// CastVoteRecordReport includes instances of GpUnit as needed. Election references GpUnit as ElectionScope, for the geographical scope of the election. CVR CastVoteRecordReport includes instances of GpUnit as needed. Election references GpUnit as ElectionScope, for the geographical scope of the election. CVR references GpUnit as BallotStyleUnit to link a CVR to the smallest political subdivision that uses the same ballot style as was used for the voter's ballot. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum GpUnitObjectType { + #[serde(rename = "CVR.GpUnit")] + #[default] + GpUnit, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct GpUnit { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: GpUnitObjectType, + + /// A code associated with the geographical unit. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Name of the geographical unit. + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Used when Type is 'other' to include a user-defined type. + #[serde(rename = "OtherType", skip_serializing_if = "Option::is_none")] + pub other_type: Option, + + /// The collection of cast vote records associated with the reporting unit and the reporting device. + #[serde(rename = "ReportingDeviceIds", skip_serializing_if = "Option::is_none")] + pub reporting_device_ids: Option>, + + /// Contains the type of geographical unit, e.g., precinct, split-precinct, vote center, using values from the ReportingUnitType enumeration. If no values apply, use 'other' and include a user-defined type in OtherType. + #[serde(rename = "Type")] + pub gp_unit_type: ReportingUnitType, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum HashObjectType { + #[serde(rename = "CVR.Hash")] + #[default] + Hash, +} + +/// Hash is used to specify a hash associated with a file such as an image file of a scanned ballot. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Hash { + #[serde(rename = "@type")] + pub object_type: HashObjectType, + + /// If Type is 'other', the type of the hash. + #[serde(rename = "OtherType", skip_serializing_if = "Option::is_none")] + pub other_type: Option, + + /// The type of the hash, from the HashType enumeration. + #[serde(rename = "Type")] + pub hash_type: HashType, + + /// The hash value, encoded as a string. + #[serde(rename = "Value")] + pub value: String, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum ImageObjectType { + #[serde(rename = "CVR.Image")] + #[default] + Image, +} + +/// Used by File for a file containing an image, e.g., an image of a write-in on a paper ballot. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Image { + #[serde(rename = "@type")] + pub object_type: ImageObjectType, + + #[serde(rename = "Data")] + pub data: String, + + /// Contains the name of the file or an identifier of the file. + #[serde(rename = "FileName", skip_serializing_if = "Option::is_none")] + pub file_name: Option, + + /// The mime type of the file, e.g., image/jpeg. + #[serde(rename = "MimeType", skip_serializing_if = "Option::is_none")] + pub mime_type: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum ImageDataObjectType { + #[serde(rename = "CVR.ImageData")] + #[default] + ImageData, +} + +/// ImageData is used to specify an image file such as for a write-in or the entire ballot. It works with several other classes, as follows: +/// +/// File with SubClass Image – to contain either a filename for an external file or the file contents, and +/// +/// Hash – to contain cryptographic hash function data for the file. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct ImageData { + #[serde(rename = "@type")] + pub object_type: ImageDataObjectType, + + /// A hash value for the image data, used for verification comparisons against subsequent copies of the image. + #[serde(rename = "Hash", skip_serializing_if = "Option::is_none")] + pub hash: Option, + + /// The image of an individual ballot sheet created by the scanner, could possibly include both sides of a two-sided ballot sheet depending on the scanner's configuration. + #[serde(rename = "Image", skip_serializing_if = "Option::is_none")] + pub image: Option, + + /// A pointer to the location of the image file. + #[serde(rename = "Location", skip_serializing_if = "Option::is_none")] + pub location: Option, +} + +/// Party is used for describing information about a political party associated with the voter's ballot. CVR includes instances of Party as needed, e.g., for a CVR corresponding to a ballot in a partisan primary, and CandidateContest references Party as needed to link a candidate to their political party. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum PartyObjectType { + #[serde(rename = "CVR.Party")] + #[default] + Party, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct Party { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: PartyObjectType, + + /// Short name for the party, e.g., "DEM". + #[serde(rename = "Abbreviation", skip_serializing_if = "Option::is_none")] + pub abbreviation: Option, + + /// A code associated with the party. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Official full name of the party, e.g., "Republican". + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +/// PartyContest is a subclass of Contest and is used to identify the type of contest as involving a straight party selection. It inherits attributes from Contest. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum PartyContestObjectType { + #[serde(rename = "CVR.PartyContest")] + #[default] + PartyContest, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct PartyContest { + #[serde(rename = "@id")] + pub id: String, + + // #[serde(rename = "@type")] + // pub object_type: PartyContestObjectType, + /// An abbreviation associated with the contest. + #[serde(rename = "Abbreviation", skip_serializing_if = "Option::is_none")] + pub abbreviation: Option, + + /// A code or identifier used for this contest. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Identifies the contest selections in the contest. + #[serde(rename = "ContestSelection")] + pub contest_selection: Vec, + + /// Title or name of the contest, e.g., "Governor" or "Question on Legalization of Gambling". + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// If VoteVariation is 'other', the vote variation for this contest. + #[serde(rename = "OtherVoteVariation", skip_serializing_if = "Option::is_none")] + pub other_vote_variation: Option, + + /// The vote variation for this contest, from the VoteVariation enumeration. + #[serde(rename = "VoteVariation", skip_serializing_if = "Option::is_none")] + pub vote_variation: Option, +} + +/// PartySelection is a subclass of ContestSelection and is used typically for a contest selection in a straight-party contest. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum PartySelectionObjectType { + #[serde(rename = "CVR.PartySelection")] + #[default] + PartySelection, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct PartySelection { + #[serde(rename = "@id")] + pub id: String, + + // #[serde(rename = "@type")] + // pub object_type: PartySelectionObjectType, + /// Code used to identify the contest selection. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// The party associated with the contest selection. + #[serde(rename = "PartyIds")] + pub party_ids: Vec, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum ReportingDeviceObjectType { + #[serde(rename = "CVR.ReportingDevice")] + #[default] + ReportingDeviceType, +} + +/// ReportingDevice is used to specify a voting device as the "political geography" at hand. CastVoteRecordReport refers to it as ReportGeneratingDevice and uses it to specify the device that created the CVR report. CVR refers to it as CreatingDevice to specify the device that created the CVRs. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct ReportingDevice { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: ReportingDeviceObjectType, + + /// The application associated with the reporting device. + #[serde(rename = "Application", skip_serializing_if = "Option::is_none")] + pub application: Option, + + /// A code associated with the reporting device. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Manufacturer of the reporting device. + #[serde(rename = "Manufacturer", skip_serializing_if = "Option::is_none")] + pub manufacturer: Option, + + /// The type of metric being used to determine quality. The type must be specific enough that the attached value can be accurately verified later, e.g., 'Acme Mark Density' may be a sufficiently specific type. + #[serde(rename = "MarkMetricType", skip_serializing_if = "Option::is_none")] + pub mark_metric_type: Option, + + /// Manufacturer's model of the reporting device. + #[serde(rename = "Model", skip_serializing_if = "Option::is_none")] + pub model: Option, + + /// Additional explanatory notes as applicable. + #[serde(rename = "Notes", skip_serializing_if = "Option::is_none")] + pub notes: Option>, + + /// Serial number or other identification that can uniquely identify the reporting device. + #[serde(rename = "SerialNumber", skip_serializing_if = "Option::is_none")] + pub serial_number: Option, +} + +/// RetentionContest is a subclass of BallotMeasureContest and is used to identify the type of contest as involving a retention, such as for a judicial retention. While it is similar to BallotMeasureContest, it contains a link to Candidate that BallotMeasureContest does not. RetentionContest inherits attributes from Contest. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum RetentionContestObjectType { + #[serde(rename = "CVR.RetentionContest")] + #[default] + RetentionContest, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize)] +pub struct RetentionContest { + #[serde(rename = "@id")] + pub id: String, + + #[serde(rename = "@type")] + pub object_type: RetentionContestObjectType, + + /// An abbreviation associated with the contest. + #[serde(rename = "Abbreviation", skip_serializing_if = "Option::is_none")] + pub abbreviation: Option, + + /// Identifies the candidate in the retention contest. + #[serde(rename = "CandidateId", skip_serializing_if = "Option::is_none")] + pub candidate_id: Option, + + /// A code or identifier used for this contest. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// Identifies the contest selections in the contest. + #[serde(rename = "ContestSelection")] + pub contest_selection: Vec, + + /// Title or name of the contest, e.g., "Governor" or "Question on Legalization of Gambling". + #[serde(rename = "Name", skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// If VoteVariation is 'other', the vote variation for this contest. + #[serde(rename = "OtherVoteVariation", skip_serializing_if = "Option::is_none")] + pub other_vote_variation: Option, + + /// The vote variation for this contest, from the VoteVariation enumeration. + #[serde(rename = "VoteVariation", skip_serializing_if = "Option::is_none")] + pub vote_variation: Option, +} + +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub enum SelectionPositionObjectType { + #[serde(rename = "CVR.SelectionPosition")] + #[default] + SelectionPosition, +} + +/// CVRContestSelection includes SelectionPosition to specify a voter's indication/mark in a contest option, and thus, a potential vote. The number of potential SelectionPositions that could be included by CVRContestSelection is the same as the number of ovals next to a particular option. There will be usually 1 instance of SelectionPosition for plurality voting, but there could be multiple instances for RCV, approval, cumulative, or other vote variations in which a voter can select multiple options per candidate. MarkMetricValue specifies the measurement of a mark on a paper ballot. The measurement is assigned by the scanner for measurements of mark density or quality and would be used by the scanner to indicate whether the mark is a valid voter mark representing a vote or is marginal.SelectionPosition contains additional information about the mark to specify whether the indication/mark is allocable, as well as information needed for certain voting methods.SelectionPosition includes CVRWriteIn, whose attributes are used to include information about the write-in including the text of the write-in or an image of the write-in. +#[derive(Clone, Eq, Hash, PartialEq, Debug, Deserialize, Serialize, Default)] +pub struct SelectionPosition { + #[serde(rename = "@type")] + pub object_type: SelectionPositionObjectType, + + /// Used to store information regarding a write-in vote. + #[serde(rename = "CVRWriteIn", skip_serializing_if = "Option::is_none")] + pub cvr_write_in: Option, + + /// Code used to identify the contest selection position. + #[serde(rename = "Code", skip_serializing_if = "Option::is_none")] + pub code: Option>, + + /// The proper fractional number of votes represented by the position. + #[serde(rename = "FractionalVotes", skip_serializing_if = "Option::is_none")] + pub fractional_votes: Option, + + /// Whether there is a selection indication present. + #[serde(rename = "HasIndication")] + pub has_indication: IndicationStatus, + + /// Whether this indication should be allocated to the contest option's accumulator. + #[serde(rename = "IsAllocable", skip_serializing_if = "Option::is_none")] + pub is_allocable: Option, + + /// Whether or not the indication was generated, rather than directly made by the voter. + #[serde(rename = "IsGenerated", skip_serializing_if = "Option::is_none")] + pub is_generated: Option, + + /// The value of the mark metric, represented as a string. + #[serde(rename = "MarkMetricValue", skip_serializing_if = "Option::is_none")] + pub mark_metric_value: Option>, + + /// The number of votes represented by the position, usually 1 but may be more depending on the voting method. + #[serde(rename = "NumberVotes")] + pub number_votes: i64, + + /// Used when Status is 'other' to include a user-defined status. + #[serde(rename = "OtherStatus", skip_serializing_if = "Option::is_none")] + pub other_status: Option, + + /// The ordinal position of the selection position within the contest option. + #[serde(rename = "Position", skip_serializing_if = "Option::is_none")] + pub position: Option, + + /// For the RCV voting variation, the rank chosen by the voter, for when a position can represent a ranking. + #[serde(rename = "Rank", skip_serializing_if = "Option::is_none")] + pub rank: Option, + + /// Status of the position, e.g., "generated-rules" for generated by the machine, from the PositionStatus enumeration. If no values apply, use 'other' and include a user-defined status in OtherStatus. + #[serde(rename = "Status", skip_serializing_if = "Option::is_none")] + pub status: Option>, +} diff --git a/libs/types-rs/src/cdf/mod.rs b/libs/types-rs/src/cdf/mod.rs new file mode 100644 index 0000000000..f50944d522 --- /dev/null +++ b/libs/types-rs/src/cdf/mod.rs @@ -0,0 +1 @@ +pub mod cvr; diff --git a/libs/types-rs/src/election.rs b/libs/types-rs/src/election.rs new file mode 100644 index 0000000000..1bb97c8af6 --- /dev/null +++ b/libs/types-rs/src/election.rs @@ -0,0 +1,610 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use hmac_sha256::Hash; +#[cfg(feature = "sqlx")] +use sqlx::Type; +use std::{fmt::Display, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +use crate::{ballot_card::BallotSide, geometry::GridUnit, util::idtype}; + +idtype!(BallotStyleId); +idtype!(CandidateId); +idtype!(ContestId); +idtype!(DistrictId); +idtype!(OptionId); +idtype!(PartyId); +idtype!(PrecinctId); + +#[derive(Debug, Clone, PartialEq)] +pub struct ElectionDefinition { + pub election: Election, + pub election_data: Vec, + pub election_hash: ElectionHash, +} + +impl FromStr for ElectionDefinition { + type Err = serde_json::Error; + + fn from_str(election_data: &str) -> Result { + election_data.as_bytes().try_into() + } +} + +impl TryFrom<&[u8]> for ElectionDefinition { + type Error = serde_json::Error; + + fn try_from(election_data: &[u8]) -> Result { + let election: Election = serde_json::from_slice(election_data)?; + + let mut hasher = Hash::new(); + hasher.update(election_data); + let election_hash = ElectionHash(hex::encode(hasher.finalize())); + + Ok(Self { + election, + election_data: election_data.to_vec(), + election_hash, + }) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for ElectionDefinition +where + sqlx::types::Json: sqlx::Decode<'r, sqlx::Postgres>, +{ + fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result { + let election_data_bytes = value.as_bytes()?; + serde_json::from_slice(election_data_bytes).map_err(Into::into) + } +} + +#[cfg(feature = "sqlx")] +impl<'q, DB> sqlx::Encode<'q, DB> for ElectionDefinition +where + Vec: sqlx::Encode<'q, DB>, + DB: sqlx::Database, +{ + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer, + ) -> sqlx::encode::IsNull { + self.election_data.encode_by_ref(buf) + } +} + +#[cfg(feature = "sqlx")] +impl Type for ElectionDefinition { + fn type_info() -> sqlx::postgres::PgTypeInfo { + sqlx::postgres::PgTypeInfo::with_name("bytea") + } +} + +impl ElectionDefinition { + pub fn from_file>(path: P) -> color_eyre::Result { + let election_data = std::fs::read_to_string(path)?; + Self::from_str(&election_data).map_err(Into::into) + } +} + +impl Serialize for ElectionDefinition { + fn serialize(&self, serializer: S) -> Result { + let election_data = STANDARD.encode(&self.election_data); + election_data.serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for ElectionDefinition { + fn deserialize>(deserializer: D) -> Result { + let election_data = String::deserialize(deserializer)?; + let election_data = STANDARD + .decode(election_data.as_str()) + .map_err(serde::de::Error::custom)?; + election_data + .as_slice() + .try_into() + .map_err(serde::de::Error::custom) + } +} + +#[derive(Debug, Clone, PartialEq)] +#[repr(transparent)] +pub struct ElectionHash(String); + +impl ElectionHash { + pub const EXPECTED_BYTE_LENGTH: usize = 32; + pub const EXPECTED_STRING_LENGTH: usize = Self::EXPECTED_BYTE_LENGTH * 2; + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn to_bytes(&self) -> Vec { + match hex::decode(&self.0) { + Ok(bytes) => bytes, + Err(_) => unreachable!("ElectionHash is always a valid hex string"), + } + } + + pub fn to_partial(&self) -> PartialElectionHash { + PartialElectionHash(self.0[..PartialElectionHash::EXPECTED_STRING_LENGTH].to_string()) + } +} + +impl TryFrom<[u8; Self::EXPECTED_BYTE_LENGTH]> for ElectionHash { + type Error = hex::FromHexError; + + fn try_from(value: [u8; Self::EXPECTED_BYTE_LENGTH]) -> Result { + Ok(Self(hex::encode(value))) + } +} + +impl FromStr for ElectionHash { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + if s.len() != Self::EXPECTED_STRING_LENGTH { + return Err(hex::FromHexError::InvalidStringLength); + } + + let hex = hex::decode(s)?; + Ok(Self(hex::encode(hex))) + } +} + +impl std::fmt::Display for ElectionHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for ElectionHash { + fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result { + Ok(Self(value.as_str()?.to_string())) + } +} + +#[cfg(feature = "sqlx")] +impl<'q, DB> sqlx::Encode<'q, DB> for ElectionHash +where + String: sqlx::Encode<'q, DB>, + DB: sqlx::Database, +{ + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer, + ) -> sqlx::encode::IsNull { + self.0.encode_by_ref(buf) + } +} + +#[cfg(feature = "sqlx")] +impl Type for ElectionHash { + fn type_info() -> sqlx::postgres::PgTypeInfo { + sqlx::postgres::PgTypeInfo::with_name("varchar") + } +} + +impl<'de> Deserialize<'de> for ElectionHash { + fn deserialize>(deserializer: D) -> Result { + let election_hash = String::deserialize(deserializer)?; + Self::from_str(&election_hash).map_err(serde::de::Error::custom) + } +} + +impl Serialize for ElectionHash { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PartialElectionHash(String); + +impl PartialElectionHash { + pub const EXPECTED_BYTE_LENGTH: usize = 10; + pub const EXPECTED_STRING_LENGTH: usize = Self::EXPECTED_BYTE_LENGTH * 2; + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn to_bytes(&self) -> Vec { + match hex::decode(self.0.as_str()) { + Ok(bytes) => bytes, + Err(_) => unreachable!("PartialElectionHash is always a valid hex string"), + } + } + + pub fn matches_election_hash(&self, election_hash: &ElectionHash) -> bool { + election_hash.as_str().starts_with(self.as_str()) + } +} + +impl TryFrom<[u8; Self::EXPECTED_BYTE_LENGTH]> for PartialElectionHash { + type Error = hex::FromHexError; + + fn try_from(value: [u8; Self::EXPECTED_BYTE_LENGTH]) -> Result { + Ok(Self(hex::encode(value))) + } +} + +impl FromStr for PartialElectionHash { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + if s.len() != PartialElectionHash::EXPECTED_STRING_LENGTH { + return Err(hex::FromHexError::InvalidStringLength); + } + + let hex = hex::decode(s)?; + assert_eq!(hex.len(), PartialElectionHash::EXPECTED_BYTE_LENGTH); + Ok(Self(hex::encode(hex))) + } +} + +impl std::fmt::Display for PartialElectionHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}…", &self.0) + } +} + +impl<'de> Deserialize<'de> for PartialElectionHash { + fn deserialize>(deserializer: D) -> Result { + let election_hash = String::deserialize(deserializer)?; + Self::from_str(&election_hash).map_err(serde::de::Error::custom) + } +} + +impl Serialize for PartialElectionHash { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Election { + pub title: String, + #[serde(with = "time::serde::iso8601")] + pub date: time::OffsetDateTime, + pub ballot_styles: Vec, + pub precincts: Vec, + pub contests: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub grid_layouts: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub mark_thresholds: Option, +} + +impl Election { + pub fn get_contests(&self, ballot_style_id: BallotStyleId) -> Option> { + let districts = &self + .ballot_styles + .iter() + .find(|ballot_style| ballot_style.id == ballot_style_id)? + .districts; + Some( + self.contests + .iter() + .filter(|contest| districts.contains(contest.district_id())) + .collect(), + ) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BallotStyle { + pub id: BallotStyleId, + pub precincts: Vec, + pub districts: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub party_id: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Precinct { + pub id: PrecinctId, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum Contest { + #[serde(rename = "candidate")] + Candidate(CandidateContest), + #[serde(rename = "yesno")] + YesNo(YesNoContest), +} + +impl Contest { + pub fn id(&self) -> &ContestId { + match self { + Self::Candidate(contest) => &contest.id, + Self::YesNo(contest) => &contest.id, + } + } + + pub fn district_id(&self) -> &DistrictId { + match self { + Self::Candidate(contest) => &contest.district_id, + Self::YesNo(contest) => &contest.district_id, + } + } + + pub fn option_ids(&self) -> Vec { + match self { + Self::Candidate(contest) => contest + .candidates + .iter() + .map(|candidate| OptionId::from(candidate.id.to_string())) + .collect(), + Self::YesNo(contest) => vec![ + contest + .yes_option + .as_ref() + .map_or(OptionId::from("yes".to_string()), |o| o.id.clone()), + contest + .no_option + .as_ref() + .map_or(OptionId::from("no".to_string()), |o| o.id.clone()), + ], + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct CandidateContest { + pub id: ContestId, + pub district_id: DistrictId, + pub title: String, + pub seats: u32, + pub candidates: Vec, + pub allow_write_ins: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub party_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Candidate { + pub id: CandidateId, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub party_ids: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_write_in: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub write_in_index: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct YesNoContest { + pub id: ContestId, + pub district_id: DistrictId, + pub title: String, + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub yes_option: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub no_option: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct YesNoOption { + pub id: OptionId, + pub label: String, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GridLayout { + pub precinct_id: PrecinctId, + pub ballot_style_id: BallotStyleId, + pub columns: GridUnit, + pub rows: GridUnit, + pub option_bounds_from_target_mark: Outset, + pub grid_positions: Vec, +} + +impl GridLayout { + pub fn write_in_positions(&self) -> Vec<&GridPosition> { + self.grid_positions + .iter() + .filter(|grid_position| matches!(grid_position, GridPosition::WriteIn { .. })) + .collect() + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Outset { + pub top: T, + pub right: T, + pub bottom: T, + pub left: T, +} + +/// A position on the ballot grid defined by timing marks and the contest/option +/// for which a mark at this position is a vote for. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum GridPosition { + /// A pre-defined labeled option on the ballot. + #[serde(rename_all = "camelCase", rename = "option")] + Option { + side: BallotSide, + column: GridUnit, + row: GridUnit, + contest_id: ContestId, + option_id: OptionId, + }, + + /// A write-in option on the ballot. + #[serde(rename_all = "camelCase", rename = "write-in")] + WriteIn { + side: BallotSide, + column: GridUnit, + row: GridUnit, + contest_id: ContestId, + write_in_index: u32, + }, +} + +impl Display for GridPosition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Option { option_id, .. } => write!(f, "{option_id}"), + + Self::WriteIn { write_in_index, .. } => { + write!(f, "Write-In {write_in_index}") + } + } + } +} + +impl GridPosition { + pub fn contest_id(&self) -> ContestId { + match self { + Self::Option { contest_id, .. } | Self::WriteIn { contest_id, .. } => { + contest_id.clone() + } + } + } + + pub fn option_id(&self) -> OptionId { + match self { + Self::Option { option_id, .. } => option_id.clone(), + Self::WriteIn { write_in_index, .. } => { + OptionId::from(format!("write-in-{write_in_index}")) + } + } + } + + pub const fn location(&self) -> GridLocation { + match self { + Self::Option { + side, column, row, .. + } + | Self::WriteIn { + side, column, row, .. + } => GridLocation::new(*side, *column, *row), + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub struct GridLocation { + pub side: BallotSide, + pub column: GridUnit, + pub row: GridUnit, +} + +impl GridLocation { + pub const fn new(side: BallotSide, column: GridUnit, row: GridUnit) -> Self { + Self { side, column, row } + } +} + +/// A value between 0 and 1, inclusive. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type UnitIntervalValue = f32; + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MarkThresholds { + pub definite: UnitIntervalValue, + pub marginal: UnitIntervalValue, + pub write_in_text_area: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_grid_location() { + let location = GridLocation::new(BallotSide::Front, 1, 2); + assert_eq!(location.side, BallotSide::Front); + assert_eq!(location.column, 1); + assert_eq!(location.row, 2); + } + + #[test] + fn test_grid_position() { + let position = GridPosition::Option { + side: BallotSide::Front, + column: 1, + row: 2, + contest_id: ContestId::from("contest-1".to_string()), + option_id: OptionId::from("option-1".to_string()), + }; + assert_eq!(position.location().side, BallotSide::Front); + assert_eq!(position.location().column, 1); + assert_eq!(position.location().row, 2); + } + + #[test] + fn test_grid_position_option_serialization() { + let json = r#"{ + "type": "option", + "side": "front", + "column": 1, + "row": 2, + "contestId": "contest-1", + "optionId": "option-1" + }"#; + match serde_json::from_str(json).unwrap() { + GridPosition::Option { + side, + column, + row, + contest_id, + option_id, + } => { + assert_eq!(side, BallotSide::Front); + assert_eq!(column, 1); + assert_eq!(row, 2); + assert_eq!(contest_id, ContestId::from("contest-1".to_string())); + assert_eq!(option_id, OptionId::from("option-1".to_string())); + } + _ => panic!("expected Option"), + } + } + + #[test] + fn test_grid_position_write_in_serialization() { + let json = r#"{ + "type": "write-in", + "side": "front", + "column": 1, + "row": 2, + "contestId": "contest-1", + "writeInIndex": 3 + }"#; + match serde_json::from_str(json).unwrap() { + GridPosition::WriteIn { + side, + column, + row, + contest_id, + write_in_index, + } => { + assert_eq!(side, BallotSide::Front); + assert_eq!(column, 1); + assert_eq!(row, 2); + assert_eq!(contest_id, ContestId::from("contest-1".to_string())); + assert_eq!(write_in_index, 3); + } + _ => panic!("expected WriteIn"), + } + } +} diff --git a/libs/types-rs/src/geometry.rs b/libs/types-rs/src/geometry.rs new file mode 100644 index 0000000000..4cf2159dfc --- /dev/null +++ b/libs/types-rs/src/geometry.rs @@ -0,0 +1,230 @@ +use std::ops::{Add, AddAssign, Sub}; + +use serde::Serialize; + +/// A unit of length in timing mark grid, i.e. 1 `GridUnit` is the logical +/// distance from one timing mark to the next. This does not map directly to +/// pixels. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type GridUnit = u32; + +/// An x or y coordinate in pixels. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type PixelPosition = i32; + +/// A width or height in pixels. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type PixelUnit = u32; + +/// A sub-pixel coordinate or distance of pixels. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type SubPixelUnit = f32; + +/// Angle in radians. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type Radians = f32; + +/// Fractional number of inches. +/// +/// Because this is just a type alias it does not enforce that another type +/// with the same underlying representation is not used. +pub type Inch = f32; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct Point> { + pub x: T, + pub y: T, +} + +impl> Point { + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } +} + +impl + Add> Add for Point { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self::new(self.x + other.x, self.y + other.y) + } +} + +impl + AddAssign + Copy> AddAssign for Point { + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl Point { + pub fn round(self) -> Point { + Point::new( + self.x.round() as PixelPosition, + self.y.round() as PixelPosition, + ) + } +} + +/// A rectangle area of pixels within an image. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct Rect { + left: PixelPosition, + top: PixelPosition, + width: PixelUnit, + height: PixelUnit, +} + +impl Rect { + pub const fn new( + left: PixelPosition, + top: PixelPosition, + width: PixelUnit, + height: PixelUnit, + ) -> Self { + Self { + left, + top, + width, + height, + } + } + + pub const fn from_points( + top_left: Point, + bottom_right: Point, + ) -> Self { + Self::new( + top_left.x, + top_left.y, + (bottom_right.x - top_left.x + 1) as PixelUnit, + (bottom_right.y - top_left.y + 1) as PixelUnit, + ) + } + + pub const fn left(&self) -> PixelPosition { + self.left + } + + pub const fn top(&self) -> PixelPosition { + self.top + } + + pub const fn width(&self) -> PixelUnit { + self.width + } + + pub const fn height(&self) -> PixelUnit { + self.height + } + + pub const fn right(&self) -> PixelPosition { + self.left + self.width as PixelPosition - 1 + } + + pub const fn bottom(&self) -> PixelPosition { + self.top + self.height as PixelPosition - 1 + } + + pub const fn offset(&self, dx: PixelPosition, dy: PixelPosition) -> Self { + Self::new(self.left + dx, self.top + dy, self.width, self.height) + } + + pub const fn top_left(&self) -> Point { + Point::new(self.left, self.top) + } + + pub const fn bottom_right(&self) -> Point { + Point::new(self.right(), self.bottom()) + } + + pub fn center(&self) -> Point { + Point::new( + self.left() as SubPixelUnit + + (self.right() as SubPixelUnit - self.left() as SubPixelUnit) / 2.0, + self.top() as SubPixelUnit + + (self.bottom() as SubPixelUnit - self.top() as SubPixelUnit) / 2.0, + ) + } + + pub fn intersect(&self, other: &Self) -> Option { + let left = self.left.max(other.left); + let top = self.top.max(other.top); + let right = self.right().min(other.right()); + let bottom = self.bottom().min(other.bottom()); + if left <= right && top <= bottom { + Some(Self::new( + left, + top, + (right - left + 1) as PixelUnit, + (bottom - top + 1) as PixelUnit, + )) + } else { + None + } + } + + // Returns the smallest rectangle that contains both `self` and `other`. + pub fn union(&self, other: &Self) -> Self { + let left = self.left.min(other.left); + let top = self.top.min(other.top); + let right = self.right().max(other.right()); + let bottom = self.bottom().max(other.bottom()); + Self::new( + left, + top, + (right - left + 1) as PixelUnit, + (bottom - top + 1) as PixelUnit, + ) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)] +pub struct Size { + pub width: T, + pub height: T, +} + +#[cfg(test)] +mod normalize_center_of_rect { + use super::*; + use proptest::prelude::*; + + #[test] + fn test_center_of_rect() { + let rect = super::Rect::new(0, 0, 10, 10); + let center = rect.center(); + assert_eq!(center.x, 4.5); + assert_eq!(center.y, 4.5); + } + + #[test] + fn test_center_of_rect_with_odd_dimensions() { + let rect = super::Rect::new(0, 0, 11, 11); + let center = rect.center(); + assert_eq!(center.x, 5.0); + assert_eq!(center.y, 5.0); + } + + proptest! { + #[test] + fn prop_center_of_rect_is_in_rect(x in 0i32..100i32, y in 0i32..100i32, width in 1u32..100u32, height in 1u32..100u32) { + let rect = super::Rect::new(x, y, width, height); + let center = rect.center(); + prop_assert!((rect.left() as SubPixelUnit) <= center.x); + prop_assert!(center.x <= (rect.right() as SubPixelUnit)); + prop_assert!((rect.top() as SubPixelUnit) <= center.y); + prop_assert!(center.y <= (rect.bottom() as SubPixelUnit)); + } + } +} diff --git a/libs/types-rs/src/lib.rs b/libs/types-rs/src/lib.rs new file mode 100644 index 0000000000..1da0c28ec9 --- /dev/null +++ b/libs/types-rs/src/lib.rs @@ -0,0 +1,8 @@ +pub mod ballot_card; +pub mod cdf; +pub mod election; +pub mod geometry; +pub mod rave; +pub mod scan; +pub mod util; +pub mod votes; diff --git a/libs/types-rs/src/rave/client/input.rs b/libs/types-rs/src/rave/client/input.rs new file mode 100644 index 0000000000..bc3cb88f48 --- /dev/null +++ b/libs/types-rs/src/rave/client/input.rs @@ -0,0 +1,76 @@ +use base64_serde::base64_serde_type; +use serde::{Deserialize, Serialize}; + +use crate::election::ElectionDefinition; +use crate::rave::ClientId; + +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Admin { + pub machine_id: String, + pub common_access_card_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationRequest { + pub client_id: ClientId, + pub machine_id: String, + pub common_access_card_id: String, + pub given_name: String, + pub family_name: String, + pub address_line_1: String, + #[serde(default)] + pub address_line_2: Option, + pub city: String, + pub state: String, + pub postal_code: String, + pub state_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Election { + pub client_id: ClientId, + pub machine_id: String, + pub definition: ElectionDefinition, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Registration { + pub client_id: ClientId, + pub machine_id: String, + pub common_access_card_id: String, + pub registration_request_id: ClientId, + pub election_id: ClientId, + pub precinct_id: String, + pub ballot_style_id: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PrintedBallot { + pub client_id: ClientId, + pub machine_id: String, + pub common_access_card_id: String, + #[serde(with = "Base64Standard")] + pub common_access_card_certificate: Vec, + pub registration_id: ClientId, + #[serde(with = "Base64Standard")] + pub cast_vote_record: Vec, + #[serde(with = "Base64Standard")] + pub cast_vote_record_signature: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ScannedBallot { + pub client_id: ClientId, + pub machine_id: String, + pub election_id: ClientId, + #[serde(with = "Base64Standard")] + pub cast_vote_record: Vec, +} diff --git a/libs/types-rs/src/rave/client/mod.rs b/libs/types-rs/src/rave/client/mod.rs new file mode 100644 index 0000000000..7d1a54844d --- /dev/null +++ b/libs/types-rs/src/rave/client/mod.rs @@ -0,0 +1,2 @@ +pub mod input; +pub mod output; diff --git a/libs/types-rs/src/rave/client/output.rs b/libs/types-rs/src/rave/client/output.rs new file mode 100644 index 0000000000..87afed63a3 --- /dev/null +++ b/libs/types-rs/src/rave/client/output.rs @@ -0,0 +1,128 @@ +use base64_serde::base64_serde_type; +use serde::{Deserialize, Serialize}; + +use crate::election::{ElectionDefinition, ElectionHash}; +use crate::rave::{ClientId, ServerId}; + +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Admin { + pub machine_id: String, + pub common_access_card_id: String, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationRequest { + pub server_id: ServerId, + pub client_id: ClientId, + pub machine_id: String, + pub common_access_card_id: String, + pub given_name: String, + pub family_name: String, + pub address_line_1: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub address_line_2: Option, + pub city: String, + pub state: String, + pub postal_code: String, + pub state_id: String, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Registration { + pub server_id: ServerId, + pub client_id: ClientId, + pub machine_id: String, + pub common_access_card_id: String, + pub registration_request_id: ServerId, + pub election_id: ServerId, + pub precinct_id: String, + pub ballot_style_id: String, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Election { + pub server_id: ServerId, + pub client_id: ClientId, + pub machine_id: String, + pub definition: ElectionDefinition, + pub election_hash: ElectionHash, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PrintedBallot { + pub server_id: ServerId, + pub client_id: ClientId, + pub machine_id: String, + pub common_access_card_id: String, + #[serde(with = "Base64Standard")] + pub common_access_card_certificate: Vec, + pub registration_id: ServerId, + #[serde(with = "Base64Standard")] + pub cast_vote_record: Vec, + #[serde(with = "Base64Standard")] + pub cast_vote_record_signature: Vec, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ScannedBallot { + pub server_id: ServerId, + pub client_id: ClientId, + pub machine_id: String, + pub election_id: ServerId, + #[serde(with = "Base64Standard")] + pub cast_vote_record: Vec, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::{engine::general_purpose::STANDARD, Engine}; + use pretty_assertions::assert_eq; + + #[test] + fn test_printed_ballot_serialization() { + let printed_ballot = PrintedBallot { + server_id: ServerId::new(), + client_id: ClientId::new(), + machine_id: "machine-1".to_string(), + common_access_card_id: "card-1".to_string(), + common_access_card_certificate: vec![9, 9, 9], + registration_id: ServerId::new(), + cast_vote_record: vec![1, 2, 3], + cast_vote_record_signature: vec![4, 5, 6], + created_at: time::OffsetDateTime::now_utc(), + }; + + let serialized = serde_json::to_string(&printed_ballot).unwrap(); + let expected_cast_vote_record = STANDARD.encode(&printed_ballot.cast_vote_record); + + assert!( + serialized.contains(&expected_cast_vote_record), + "serialized: {serialized}", + ); + assert_eq!( + serde_json::from_str::(&serialized).unwrap(), + printed_ballot, + ); + } +} diff --git a/libs/types-rs/src/rave/jx/mod.rs b/libs/types-rs/src/rave/jx/mod.rs new file mode 100644 index 0000000000..e63600ffef --- /dev/null +++ b/libs/types-rs/src/rave/jx/mod.rs @@ -0,0 +1,278 @@ +use base64_serde::base64_serde_type; +use serde::{Deserialize, Serialize}; + +use crate::{ + cdf::cvr::Cvr, + election::{BallotStyle, BallotStyleId, ElectionHash, PrecinctId}, +}; + +use super::{ClientId, ServerId}; + +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Election { + pub id: ClientId, + pub server_id: Option, + pub title: String, + pub date: time::Date, + pub ballot_styles: Vec, + pub election_hash: ElectionHash, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +impl Election { + pub const fn new( + id: ClientId, + server_id: Option, + title: String, + date: time::Date, + ballot_styles: Vec, + election_hash: ElectionHash, + created_at: time::OffsetDateTime, + ) -> Self { + Self { + id, + server_id, + title, + date, + ballot_styles, + election_hash, + created_at, + } + } + + pub fn id(&self) -> &ClientId { + &self.id + } + + pub fn is_synced(&self) -> bool { + self.server_id.is_some() + } + + pub fn created_at(&self) -> &time::OffsetDateTime { + &self.created_at + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RegistrationRequest { + id: ClientId, + server_id: ServerId, + common_access_card_id: String, + display_name: String, + #[serde(with = "time::serde::iso8601")] + created_at: time::OffsetDateTime, +} + +impl RegistrationRequest { + pub const fn new( + id: ClientId, + server_id: ServerId, + common_access_card_id: String, + display_name: String, + created_at: time::OffsetDateTime, + ) -> Self { + Self { + id, + server_id, + common_access_card_id, + display_name, + created_at, + } + } + + pub fn id(&self) -> &ClientId { + &self.id + } + + pub fn common_access_card_id(&self) -> &str { + &self.common_access_card_id + } + + pub fn display_name(&self) -> &str { + &self.display_name + } + + pub fn created_at(&self) -> &time::OffsetDateTime { + &self.created_at + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Registration { + id: ClientId, + server_id: Option, + display_name: String, + common_access_card_id: String, + registration_request_id: ClientId, + election_title: String, + election_hash: ElectionHash, + precinct_id: PrecinctId, + ballot_style_id: BallotStyleId, + #[serde(with = "time::serde::iso8601")] + created_at: time::OffsetDateTime, +} + +impl Registration { + #[allow(clippy::too_many_arguments)] + pub const fn new( + id: ClientId, + server_id: Option, + display_name: String, + common_access_card_id: String, + registration_request_id: ClientId, + election_title: String, + election_hash: ElectionHash, + precinct_id: PrecinctId, + ballot_style_id: BallotStyleId, + created_at: time::OffsetDateTime, + ) -> Self { + Self { + id, + server_id, + display_name, + common_access_card_id, + registration_request_id, + election_title, + election_hash, + precinct_id, + ballot_style_id, + created_at, + } + } + + pub fn id(&self) -> &ClientId { + &self.id + } + + pub fn is_synced(&self) -> bool { + self.server_id.is_some() + } + + pub fn display_name(&self) -> &str { + self.display_name.as_str() + } + + pub fn election_title(&self) -> &str { + &self.election_title + } + + pub fn election_hash(&self) -> &ElectionHash { + &self.election_hash + } + + pub fn common_access_card_id(&self) -> &str { + &self.common_access_card_id + } + + pub fn ballot_style_id(&self) -> &BallotStyleId { + &self.ballot_style_id + } + + pub fn precinct_id(&self) -> &PrecinctId { + &self.precinct_id + } + + pub fn is_registration_request(&self, registration_request: &RegistrationRequest) -> bool { + self.registration_request_id == registration_request.id + } + + pub fn created_at(&self) -> &time::OffsetDateTime { + &self.created_at + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub enum VerificationStatus { + Success { + common_access_card_id: String, + display_name: String, + }, + Failure, + Error(String), + #[default] + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PrintedBallot { + pub id: ClientId, + pub server_id: ServerId, + pub registration_id: ClientId, + pub election_id: ClientId, + pub ballot_style_id: BallotStyleId, + pub precinct_id: PrecinctId, + #[serde(with = "Base64Standard")] + pub cast_vote_record: Vec, + #[serde(with = "Base64Standard")] + pub cast_vote_record_signature: Vec, + pub verification_status: VerificationStatus, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +impl PrintedBallot { + pub fn election_id(&self) -> &ClientId { + &self.election_id + } + + pub fn ballot_style_id(&self) -> &BallotStyleId { + &self.ballot_style_id + } + + pub fn precinct_id(&self) -> &PrecinctId { + &self.precinct_id + } + + pub fn created_at(&self) -> &time::OffsetDateTime { + &self.created_at + } + + pub fn cast_vote_record(&self) -> color_eyre::Result { + let cast_vote_record_json = serde_json::from_slice(&self.cast_vote_record)?; + Ok(cast_vote_record_json) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ScannedBallot { + pub id: ClientId, + pub server_id: ServerId, + pub election_id: ClientId, + pub precinct_id: PrecinctId, + pub ballot_style_id: BallotStyleId, + #[serde(with = "Base64Standard")] + pub cast_vote_record: Vec, + #[serde(with = "time::serde::iso8601")] + pub created_at: time::OffsetDateTime, +} + +impl ScannedBallot { + pub fn created_at(&self) -> &time::OffsetDateTime { + &self.created_at + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct AppData { + pub elections: Vec, + pub registration_requests: Vec, + pub registrations: Vec, + pub printed_ballots: Vec, + pub scanned_ballots: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateRegistrationData { + pub election_id: ClientId, + pub registration_request_id: ClientId, + pub ballot_style_id: BallotStyleId, + pub precinct_id: PrecinctId, +} diff --git a/libs/types-rs/src/rave/mod.rs b/libs/types-rs/src/rave/mod.rs new file mode 100644 index 0000000000..bed9ea01c0 --- /dev/null +++ b/libs/types-rs/src/rave/mod.rs @@ -0,0 +1,157 @@ +use serde::Deserialize; +use serde::Serialize; +use uuid::Uuid; + +pub mod client; +pub mod jx; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(transparent)] +pub struct ServerId(Uuid); + +impl ServerId { + pub fn new() -> Self { + Self::default() + } + + pub fn as_uuid(&self) -> Uuid { + self.0 + } +} + +impl Default for ServerId { + fn default() -> Self { + Self(Uuid::new_v4()) + } +} + +impl From for ServerId { + fn from(uuid: Uuid) -> Self { + Self(uuid) + } +} + +impl std::fmt::Display for ServerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "sqlx")] +impl sqlx::Type for ServerId { + fn type_info() -> sqlx::postgres::PgTypeInfo { + >::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for ServerId { + fn decode( + value: sqlx::postgres::PgValueRef<'r>, + ) -> Result> { + >::decode(value).map(Self) + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for ServerId { + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer, + ) -> sqlx::encode::IsNull { + >::encode_by_ref(&self.0, buf) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(transparent)] +pub struct ClientId(Uuid); + +impl ClientId { + pub fn new() -> Self { + Self::default() + } + + pub fn as_uuid(&self) -> Uuid { + self.0 + } +} + +impl Default for ClientId { + fn default() -> Self { + Self(Uuid::new_v4()) + } +} + +impl From for ClientId { + fn from(uuid: Uuid) -> Self { + Self(uuid) + } +} + +impl std::fmt::Display for ClientId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "sqlx")] +impl sqlx::Type for ClientId { + fn type_info() -> sqlx::postgres::PgTypeInfo { + >::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for ClientId { + fn decode( + value: sqlx::postgres::PgValueRef<'r>, + ) -> Result> { + >::decode(value).map(Self) + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for ClientId { + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer, + ) -> sqlx::encode::IsNull { + >::encode_by_ref(&self.0, buf) + } +} +#[derive(Debug, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct RaveServerSyncInput { + #[serde(default)] + pub last_synced_registration_request_id: Option, + #[serde(default)] + pub last_synced_registration_id: Option, + #[serde(default)] + pub last_synced_election_id: Option, + #[serde(default)] + pub last_synced_printed_ballot_id: Option, + #[serde(default)] + pub last_synced_scanned_ballot_id: Option, + #[serde(default)] + pub registration_requests: Vec, + #[serde(default)] + pub elections: Vec, + #[serde(default)] + pub registrations: Vec, + #[serde(default)] + pub printed_ballots: Vec, + #[serde(default)] + pub scanned_ballots: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RaveServerSyncOutput { + pub admins: Vec, + pub elections: Vec, + pub registration_requests: Vec, + pub registrations: Vec, + pub printed_ballots: Vec, + pub scanned_ballots: Vec, +} diff --git a/libs/types-rs/src/scan.rs b/libs/types-rs/src/scan.rs new file mode 100644 index 0000000000..1b24a022f5 --- /dev/null +++ b/libs/types-rs/src/scan.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +use crate::rave::ClientId; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ScannedBallotStats { + pub batches: Vec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BatchStats { + pub id: ClientId, + pub ballot_count: i32, + pub election_count: i32, + pub synced_count: i32, + #[serde(with = "time::serde::iso8601")] + pub started_at: time::OffsetDateTime, + #[serde(with = "time::serde::iso8601::option")] + pub ended_at: Option, +} diff --git a/libs/types-rs/src/util.rs b/libs/types-rs/src/util.rs new file mode 100644 index 0000000000..65bfa34888 --- /dev/null +++ b/libs/types-rs/src/util.rs @@ -0,0 +1,47 @@ +// Defines a new type that wraps a String for use as an ID. +macro_rules! idtype { + ($name:ident) => { + #[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] + pub struct $name(String); + + impl $name { + #[allow(dead_code)] + pub const fn from(s: String) -> Self { + Self(s) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } + } + }; +} + +pub(crate) use idtype; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_idtype() { + idtype!(Foo); + let foo = Foo::from("foo".to_string()); + assert_eq!(format!("{}", foo), "foo"); + assert_eq!(serde_json::to_string(&foo).unwrap(), r#""foo""#); + assert_eq!( + serde_json::from_str::(r#""foo""#).unwrap(), + Foo::from("foo".to_string()) + ); + } + + #[test] + fn test_idtype_clone() { + idtype!(Foo); + let foo = Foo::from("foo".to_string()); + let foo2 = foo.clone(); + assert_eq!(foo, foo2); + } +} diff --git a/libs/types-rs/src/votes.rs b/libs/types-rs/src/votes.rs new file mode 100644 index 0000000000..1f1567a39a --- /dev/null +++ b/libs/types-rs/src/votes.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::election::{Candidate, ContestId}; + +pub type VotesDict = HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Vote { + Candidate(CandidateVote), + YesNo(YesNoVote), +} + +pub type CandidateVote = Vec; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum YesNoVote { + Empty(), + One(YesOrNo), + Both(YesOrNo, YesOrNo), +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum YesOrNo { + #[serde(rename = "yes")] + Yes, + #[serde(rename = "no")] + No, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize() { + let votes = VotesDict::new(); + let serialized = serde_json::to_string(&votes).unwrap(); + assert_eq!(serialized, "{}"); + } + + #[test] + fn test_deserialize() { + let votes = VotesDict::new(); + let serialized = serde_json::to_string(&votes).unwrap(); + let deserialized: VotesDict = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, votes); + } +} diff --git a/libs/types/src/auth/auth.ts b/libs/types/src/auth/auth.ts index 3a9ea7b76d..f1c2769b35 100644 --- a/libs/types/src/auth/auth.ts +++ b/libs/types/src/auth/auth.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { BallotStyleId, PrecinctId } from '../election'; +import { Id } from '../generic'; export interface SystemAdministratorUser { readonly role: 'system_administrator'; @@ -42,6 +43,14 @@ export const UserRoleSchema: z.ZodSchema = z.union([ z.literal('cardless_voter'), ]); +export interface RaveVoterUser { + readonly role: 'rave_voter'; + readonly commonAccessCardId: Id; + readonly givenName: string; + readonly middleName?: string; + readonly familyName: string; +} + /** * See libs/auth/src/lockout.ts for more context. */ diff --git a/libs/types/src/printing.ts b/libs/types/src/printing.ts index bdc102d19f..39d1cd1e7b 100644 --- a/libs/types/src/printing.ts +++ b/libs/types/src/printing.ts @@ -1,5 +1,7 @@ export interface PrintOptions extends KioskBrowser.PrintOptions { + deviceName?: string; sides: KioskBrowser.PrintSides; + raw?: { [key: string]: string }; } export interface Printer { print(options: PrintOptions): Promise; diff --git a/libs/ui-rs/Cargo.toml b/libs/ui-rs/Cargo.toml new file mode 100644 index 0000000000..3b6182b849 --- /dev/null +++ b/libs/ui-rs/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ui-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = { workspace = true } +dioxus = { workspace = true } +dioxus-logger = { workspace = true } +dioxus-web = { workspace = true } +getrandom = { version = "0.2", features = ["js"] } +js-sys = { workspace = true } +log = "0.4.19" +time = { workspace = true } +wasm-bindgen = "0.2.87" +wasm-logger = "0.2.0" +web-sys = { version = "0.3.64", features = ["EventSource"] } diff --git a/libs/ui-rs/package.json b/libs/ui-rs/package.json new file mode 100644 index 0000000000..52118d3e68 --- /dev/null +++ b/libs/ui-rs/package.json @@ -0,0 +1,15 @@ +{ + "name": "@votingworks/ui-rs", + "version": "1.0.0", + "description": "Shared UI for Dioxus apps", + "scripts": { + "build": "cargo build", + "lint": "cargo clippy -- -D warnings", + "start": "cargo watch -x run", + "test": "cargo test" + }, + "private": true, + "license": "GPL-3.0", + "author": "VotingWorks ", + "packageManager": "pnpm@8.1.0" +} diff --git a/libs/ui-rs/src/button.rs b/libs/ui-rs/src/button.rs new file mode 100644 index 0000000000..e463e16ad4 --- /dev/null +++ b/libs/ui-rs/src/button.rs @@ -0,0 +1,48 @@ +use dioxus::prelude::*; + +#[derive(Debug, Clone, Copy, Default)] +pub enum ButtonSize { + ExtraLarge, + Large, + #[default] + Medium, +} + +#[derive(Props)] +pub struct Props<'a> { + size: Option, + class: Option<&'a str>, + children: Element<'a>, + disabled: Option, + onclick: Option>, +} + +const EXTRA_LARGE_CLASS: &str = "rounded-lg text-xl p-3"; +const LARGE_CLASS: &str = "rounded-md text-lg p-2"; +const MEDIUM_CLASS: &str = "rounded-md text-base p-1 px-2"; + +#[allow(non_snake_case)] +pub fn Button<'a>(cx: Scope<'a, Props<'a>>) -> Element { + let button_size_class = match cx.props.size.unwrap_or_default() { + ButtonSize::ExtraLarge => EXTRA_LARGE_CLASS, + ButtonSize::Large => LARGE_CLASS, + ButtonSize::Medium => MEDIUM_CLASS, + }; + render! { + button { + class: r#" + bg-purple-500 active:bg-purple-700 disabled:bg-purple-300 + text-white + {button_size_class} + {cx.props.class.unwrap_or_default()} + "#, + disabled: cx.props.disabled, + onclick: |e| { + if let Some(onclick) = &cx.props.onclick { + onclick.call(e); + } + }, + &cx.props.children + } + } +} diff --git a/libs/ui-rs/src/date_or_datetime_cell.rs b/libs/ui-rs/src/date_or_datetime_cell.rs new file mode 100644 index 0000000000..8c5b6f36e3 --- /dev/null +++ b/libs/ui-rs/src/date_or_datetime_cell.rs @@ -0,0 +1,30 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; + +use crate::{util::datetime, TableCell}; + +#[derive(Debug, Props, PartialEq)] +pub struct Props { + #[props(into)] + date_or_datetime: datetime::DateOrDateTime, +} + +pub fn DateOrDateTimeCell(cx: Scope) -> Element { + let date_or_datetime = cx.props.date_or_datetime.clone(); + let formatted = datetime::format( + &date_or_datetime, + match date_or_datetime { + datetime::DateOrDateTime::Date(_) => { + &datetime::DateFormatOptions::DATE_WITH_DAY_OF_WEEK + } + datetime::DateOrDateTime::DateTime(_) => &datetime::DateFormatOptions::DATETIME, + }, + ); + render!( + TableCell { + title: "{date_or_datetime.to_iso8601()}", + "{formatted}" + } + ) +} diff --git a/libs/ui-rs/src/file_button.rs b/libs/ui-rs/src/file_button.rs new file mode 100644 index 0000000000..21505368d3 --- /dev/null +++ b/libs/ui-rs/src/file_button.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use base64::{engine::general_purpose::STANDARD, Engine}; +use dioxus::prelude::*; +use wasm_bindgen::JsCast; + +use crate::button::Button; + +#[derive(Props)] +pub struct Props<'a> { + class: Option<&'a str>, + children: Element<'a>, + disabled: Option, + #[props(into)] + onfile: Option>>, +} + +/// A button that opens a file dialog when clicked. +/// +/// # Example +/// +/// ```rust,no_run +/// use std::sync::Arc; +/// use dioxus::prelude::*; +/// use ui_rs::FileButton; +/// +/// fn app(cx: Scope) -> Element { +/// render! { +/// FileButton { +/// "Click me", +/// onfile: { +/// move |file_engine: Arc| { +/// // read the first file, assuming there is at least one +/// cx.spawn(async move { +/// let files = file_engine.files(); +/// let file = files.first().unwrap(); +/// let content = file_engine.read_file(&file).await.unwrap(); +/// log::info!("file content: {:?}", content); +/// }); +/// } +/// } +/// } +/// } +/// } +/// ``` +/// +/// # Panics +/// +/// This component may panic if the `window.crypto` API is not available. +/// ``` +#[allow(non_snake_case)] +pub fn FileButton<'a>(cx: Scope<'a, Props<'a>>) -> Element { + let id = use_state(cx, || { + let mut randombytes = [0u8; 32]; + getrandom::getrandom(&mut randombytes).unwrap(); + STANDARD.encode(randombytes.as_ref()) + }); + + render! { + input { + id: id.as_ref(), + r#type: "file", + class: "hidden", + disabled: cx.props.disabled, + oninput: { + |e| { + if let Some(onfile) = &cx.props.onfile { + if let Some(file_engine) = &e.data.files { + onfile.call(file_engine.clone()); + } + } + } + }, + } + Button { + class: cx.props.class.unwrap_or(""), + &cx.props.children + onclick: { + to_owned![id]; + move |_e: MouseEvent| { + let input = web_sys::window().unwrap().document().unwrap().get_element_by_id(&id).unwrap(); + input.dyn_ref::().unwrap().click(); + } + } + } + } +} diff --git a/libs/ui-rs/src/lib.rs b/libs/ui-rs/src/lib.rs new file mode 100644 index 0000000000..c3fbd6ea78 --- /dev/null +++ b/libs/ui-rs/src/lib.rs @@ -0,0 +1,10 @@ +mod button; +mod date_or_datetime_cell; +mod file_button; +mod table_cell; +mod util; + +pub use button::Button; +pub use date_or_datetime_cell::DateOrDateTimeCell; +pub use file_button::FileButton; +pub use table_cell::TableCell; diff --git a/libs/ui-rs/src/table_cell.rs b/libs/ui-rs/src/table_cell.rs new file mode 100644 index 0000000000..e84c4de18e --- /dev/null +++ b/libs/ui-rs/src/table_cell.rs @@ -0,0 +1,17 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; + +#[derive(Debug, Props)] +pub struct Props<'a> { + title: Option<&'a str>, + children: Element<'a>, +} + +pub fn TableCell<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { + render!(td { + class: "border px-4 py-2 whitespace-nowrap", + title: cx.props.title, + &cx.props.children + }) +} diff --git a/libs/ui-rs/src/util/datetime.rs b/libs/ui-rs/src/util/datetime.rs new file mode 100644 index 0000000000..33bb2b68e7 --- /dev/null +++ b/libs/ui-rs/src/util/datetime.rs @@ -0,0 +1,145 @@ +use wasm_bindgen::JsValue; + +#[derive(Debug, Clone, PartialEq)] +pub enum DateOrDateTime { + Date(time::Date), + DateTime(time::OffsetDateTime), +} + +impl DateOrDateTime { + pub fn to_iso8601(&self) -> String { + match self { + Self::Date(date) => date.to_string(), + Self::DateTime(date_time) => date_time.to_string(), + } + } +} + +impl From for DateOrDateTime { + fn from(date: time::Date) -> Self { + Self::Date(date) + } +} + +impl From for DateOrDateTime { + fn from(date_time: time::OffsetDateTime) -> Self { + Self::DateTime(date_time) + } +} + +impl From<&time::Date> for DateOrDateTime { + fn from(date: &time::Date) -> Self { + Self::Date(*date) + } +} + +impl From<&time::OffsetDateTime> for DateOrDateTime { + fn from(date_time: &time::OffsetDateTime) -> Self { + Self::DateTime(*date_time) + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[allow(dead_code)] +pub enum DateStyle { + #[default] + Default, + Full, + Long, + Medium, + Short, +} + +impl DateStyle { + fn to_js_value(self) -> Option { + match self { + Self::Default => None, + Self::Full => Some(JsValue::from_str("full")), + Self::Long => Some(JsValue::from_str("long")), + Self::Medium => Some(JsValue::from_str("medium")), + Self::Short => Some(JsValue::from_str("short")), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[allow(dead_code)] +pub enum TimeStyle { + #[default] + Default, + Full, + Long, + Medium, + Short, +} + +impl TimeStyle { + pub fn to_js_value(self) -> Option { + match self { + Self::Default => None, + Self::Full => Some(JsValue::from_str("full")), + Self::Long => Some(JsValue::from_str("long")), + Self::Medium => Some(JsValue::from_str("medium")), + Self::Short => Some(JsValue::from_str("short")), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct DateFormatOptions { + date_style: DateStyle, + time_style: TimeStyle, +} + +#[allow(dead_code)] +impl DateFormatOptions { + pub const DATETIME: Self = Self { + date_style: DateStyle::Medium, + time_style: TimeStyle::Short, + }; + pub const DATE: Self = Self { + date_style: DateStyle::Medium, + time_style: TimeStyle::Default, + }; + pub const DATE_WITH_DAY_OF_WEEK: Self = Self { + date_style: DateStyle::Full, + time_style: TimeStyle::Default, + }; +} + +pub fn format(date_or_datetime: &DateOrDateTime, options: &DateFormatOptions) -> String { + let locales = js_sys::Array::new(); + locales.push(&JsValue::from_str("default")); + + let js_options = js_sys::Object::new(); + + if let Some(date_style) = options.date_style.to_js_value() { + let _ = js_sys::Reflect::set(&js_options, &JsValue::from_str("dateStyle"), &date_style); + } + + if let Some(time_style) = options.time_style.to_js_value() { + let _ = js_sys::Reflect::set(&js_options, &JsValue::from_str("timeStyle"), &time_style); + } + + let formatter = js_sys::Intl::DateTimeFormat::new(&locales, &js_options); + let js_date = js_sys::Date::new_0(); + + match date_or_datetime { + DateOrDateTime::Date(date) => { + js_date.set_utc_full_year(date.year() as u32); + js_date.set_utc_month(date.month() as u32 - 1); + js_date.set_utc_date(date.day() as u32); + } + DateOrDateTime::DateTime(datetime) => { + js_date.set_utc_full_year(datetime.year() as u32); + js_date.set_utc_month(datetime.month() as u32 - 1); + js_date.set_utc_date(datetime.day() as u32); + js_date.set_utc_hours(datetime.hour() as u32); + js_date.set_utc_minutes(datetime.minute() as u32); + js_date.set_utc_seconds(datetime.second() as u32); + } + } + + let formatted_date = formatter.format().call1(&formatter, &js_date).unwrap(); + formatted_date.as_string().unwrap() +} diff --git a/libs/ui-rs/src/util/mod.rs b/libs/ui-rs/src/util/mod.rs new file mode 100644 index 0000000000..afda8ae336 --- /dev/null +++ b/libs/ui-rs/src/util/mod.rs @@ -0,0 +1 @@ +pub(crate) mod datetime; diff --git a/libs/ui/src/error_boundary.tsx b/libs/ui/src/error_boundary.tsx index 9cfa306cc8..5423bcc1eb 100644 --- a/libs/ui/src/error_boundary.tsx +++ b/libs/ui/src/error_boundary.tsx @@ -31,6 +31,7 @@ export class ErrorBoundary extends React.Component { constructor(props: Props) { super(props); this.state = {}; + this.handleUnhandledRejection = this.handleUnhandledRejection.bind(this); } static getDerivedStateFromError(error: unknown): State { diff --git a/libs/utils/src/format.ts b/libs/utils/src/format.ts index f050da1eeb..8ba57289ab 100644 --- a/libs/utils/src/format.ts +++ b/libs/utils/src/format.ts @@ -13,6 +13,49 @@ export function count( return new Intl.NumberFormat(locale, { useGrouping: true }).format(value); } +function interpolate( + template: string, + values: Record +): string { + return template.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (_match, key) => + Object.hasOwn(values, key) ? `${values[key]}` : '' + ); +} + +/** + * Format a number as a count of something, with a phrase that depends on the + * count. + * + * @example + * + * countPhrase(0, { one: '1 item', many: '{{count}} items' }) // '0 items' + */ +export function countPhrase({ + value, + locale = DEFAULT_LOCALE, + zero, + one, + many, +}: { + value: number; + locale?: LanguageCode; + zero?: string; + one: string; + many: string; +}): string { + let template = ''; + if (value === 0 && zero) { + template = zero; + } else if (value === 1) { + template = one; + } else { + template = many; + } + return interpolate(template, { + count: new Intl.NumberFormat(locale).format(value), + }); +} + /** * Formats a number as a percentage. * diff --git a/mprocs.yaml b/mprocs.yaml new file mode 100644 index 0000000000..918ecb1845 --- /dev/null +++ b/mprocs.yaml @@ -0,0 +1,44 @@ +procs: + rave-server: + cwd: 'services/rave-server' + shell: 'cargo watch -x run' + env: + PORT: '8000' + DATABASE_URL: 'postgres:rave' + + rave-scan-backend: + cwd: 'apps/rave-scan/backend' + shell: 'cargo watch -x run' + env: + PORT: '4001' + DATABASE_URL: 'postgres:rave_scan' + RAVE_URL: 'http://localhost:8000/' + VX_MACHINE_ID: 'rave-scan-dev' + rave-scan-frontend: + cwd: 'apps/rave-scan/frontend' + shell: 'dx serve --port 4000' + rave-scan-frontend-css: + cwd: 'apps/rave-scan/frontend' + shell: 'pnpm tailwindcss -i input.css -o ./public/styles.css --watch' + + rave-jx-backend: + cwd: 'apps/rave-jx-terminal/backend' + shell: 'cargo watch -x run' + env: + PORT: '5001' + DATABASE_URL: 'postgres:rave_jx' + RAVE_URL: 'http://localhost:8000/' + VX_MACHINE_ID: 'rave-jx-dev' + rave-jx-frontend: + cwd: 'apps/rave-jx-terminal/frontend' + shell: 'dx serve --port 5000' + rave-jx-frontend-css: + cwd: 'apps/rave-jx-terminal/frontend' + shell: 'pnpm tailwindcss -i input.css -o ./public/styles.css --watch' + + rave-mark: + cwd: 'apps/rave-mark/frontend' + shell: 'pnpm tsc --build && pnpm start' + env: + RAVE_URL: 'http://localhost:8000/' + VX_MACHINE_ID: 'rave-mark-dev' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d77c902cda..0bd1a42598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,22 @@ importers: specifier: 5.2.2 version: 5.2.2 + apps/rave-jx-terminal/backend: + devDependencies: + is-ci-cli: + specifier: 2.2.0 + version: 2.2.0 + + apps/rave-jx-terminal/frontend: + dependencies: + tailwindcss: + specifier: ^3.3.3 + version: 3.3.3 + devDependencies: + concurrently: + specifier: 7.6.0 + version: 7.6.0 + apps/rave-mark/backend: dependencies: '@votingworks/auth': @@ -138,9 +154,6 @@ importers: '@votingworks/db': specifier: workspace:* version: link:../../../libs/db - '@votingworks/dev-dock-backend': - specifier: workspace:* - version: link:../../../libs/dev-dock/backend '@votingworks/fixtures': specifier: workspace:* version: link:../../../libs/fixtures @@ -156,8 +169,11 @@ importers: '@votingworks/utils': specifier: workspace:* version: link:../../../libs/utils + cross-fetch: + specifier: ^3.1.5 + version: 3.1.5 debug: - specifier: ^4.3.4 + specifier: 4.3.4 version: 4.3.4(supports-color@5.5.0) dotenv: specifier: ^16.1.4 @@ -166,117 +182,114 @@ importers: specifier: ^10.0.0 version: 10.0.0 express: - specifier: ^4.18.2 + specifier: 4.18.2 version: 4.18.2 fs-extra: - specifier: ^11.1.1 + specifier: 11.1.1 version: 11.1.1 + luxon: + specifier: ^3.0.0 + version: 3.3.0 + puppeteer: + specifier: ^21.1.1 + version: 21.1.1 + tmp: + specifier: ^0.2.1 + version: 0.2.1 + uuid: + specifier: 9.0.1 + version: 9.0.1 + xml: + specifier: ^1.0.1 + version: 1.0.1 zod: specifier: 3.14.4 version: 3.14.4 devDependencies: '@types/debug': - specifier: ^4.1.8 + specifier: 4.1.8 version: 4.1.8 '@types/express': - specifier: ^4.17.14 + specifier: 4.17.14 version: 4.17.14 '@types/fs-extra': - specifier: ^11.0.1 + specifier: 11.0.1 version: 11.0.1 '@types/jest': - specifier: ^29.5.2 + specifier: ^29.5.3 version: 29.5.3 + '@types/luxon': + specifier: ^3.0.0 + version: 3.2.0 + '@types/node': + specifier: 16.18.23 + version: 16.18.23 '@types/tmp': - specifier: ^0.2.3 + specifier: 0.2.4 version: 0.2.4 - '@typescript-eslint/eslint-plugin': - specifier: 5.37.0 - version: 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/parser': - specifier: 5.37.0 - version: 5.37.0(eslint@8.23.1)(typescript@5.2.2) + '@types/uuid': + specifier: 9.0.5 + version: 9.0.5 + '@types/xml': + specifier: ^1.0.9 + version: 1.0.9 '@votingworks/test-utils': specifier: workspace:* version: link:../../../libs/test-utils eslint: - specifier: 8.23.1 - version: 8.23.1 + specifier: 8.51.0 + version: 8.51.0 eslint-config-prettier: - specifier: ^8.5.0 - version: 8.5.0(eslint@8.23.1) + specifier: ^9.0.0 + version: 9.0.0(eslint@8.51.0) eslint-import-resolver-node: - specifier: ^0.3.4 + specifier: ^0.3.9 version: 0.3.9 eslint-plugin-import: specifier: ^2.26.0 - version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) eslint-plugin-jest: - specifier: ^26.1.5 - version: 26.1.5(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2) + specifier: ^27.2.3 + version: 27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2) eslint-plugin-prettier: - specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.5.0)(eslint@8.23.1)(prettier@2.6.2) + specifier: ^5.0.0 + version: 5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3) eslint-plugin-vx: specifier: workspace:* version: link:../../../libs/eslint-plugin-vx is-ci-cli: - specifier: ^2.1.2 + specifier: 2.2.0 version: 2.2.0 jest: - specifier: ^29.5.0 + specifier: ^29.6.2 version: 29.6.2(@types/node@16.18.23) jest-junit: - specifier: ^14.0.1 - version: 14.0.1 + specifier: ^16.0.0 + version: 16.0.0 jest-watch-typeahead: - specifier: ^0.6.4 - version: 0.6.4(jest@29.6.2) + specifier: ^2.2.2 + version: 2.2.2(jest@29.6.2) lint-staged: - specifier: ^10.5.3 - version: 10.5.3 + specifier: 11.0.0 + version: 11.0.0 nodemon: specifier: ^2.0.20 version: 2.0.20 prettier: - specifier: 2.6.2 - version: 2.6.2 - stylelint: - specifier: ^13.3.3 - version: 13.3.3 - stylelint-config-palantir: - specifier: ^4.0.1 - version: 4.0.1(stylelint@13.3.3) - stylelint-config-prettier: - specifier: ^8.0.1 - version: 8.0.1(stylelint@13.3.3) - stylelint-config-styled-components: - specifier: ^0.1.1 - version: 0.1.1 - stylelint-processor-styled-components: - specifier: ^1.10.0 - version: 1.10.0 - tmp: - specifier: ^0.2.1 - version: 0.2.1 + specifier: 3.0.3 + version: 3.0.3 ts-jest: - specifier: ^29.1.0 + specifier: 29.1.1 version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2) - typescript: - specifier: 5.2.2 - version: 5.2.2 apps/rave-mark/frontend: dependencies: '@tanstack/react-query': - specifier: ^4.22.0 - version: 4.32.1(react-dom@17.0.1)(react@17.0.1) + specifier: 4.32.1 + version: 4.32.1(react-dom@18.2.0)(react@18.2.0) '@votingworks/basics': specifier: workspace:* version: link:../../../libs/basics - '@votingworks/dev-dock-frontend': - specifier: workspace:^ - version: link:../../../libs/dev-dock/frontend '@votingworks/grout': specifier: workspace:* version: link:../../../libs/grout @@ -301,185 +314,155 @@ importers: buffer: specifier: ^6.0.3 version: 6.0.3 + luxon: + specifier: ^3.0.0 + version: 3.3.0 path: specifier: ^0.12.7 version: 0.12.7 react: - specifier: 17.0.1 - version: 17.0.1 + specifier: 18.2.0 + version: 18.2.0 react-dom: - specifier: 17.0.1 - version: 17.0.1(react@17.0.1) - react-router-dom: - specifier: ^5.2.0 - version: 5.3.4(react@17.0.1) + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + styled-components: + specifier: ^5.3.11 + version: 5.3.11(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) devDependencies: + '@jest/types': + specifier: ^29.6.1 + version: 29.6.1 + '@testing-library/jest-dom': + specifier: ^5.17.0 + version: 5.17.0 '@testing-library/react': - specifier: ^12.1.5 - version: 12.1.5(react-dom@17.0.1)(react@17.0.1) + specifier: ^14.0.0 + version: 14.0.0(react-dom@18.2.0)(react@18.2.0) + '@testing-library/user-event': + specifier: ^13.5.0 + version: 13.5.0(@testing-library/dom@9.3.1) '@types/jest': - specifier: ^29.5.2 + specifier: ^29.5.3 version: 29.5.3 + '@types/luxon': + specifier: ^3.0.0 + version: 3.2.0 '@types/react': - specifier: 17.0.39 - version: 17.0.39 + specifier: 18.2.18 + version: 18.2.18 '@types/react-dom': - specifier: ^18.2.4 + specifier: ^18.2.7 version: 18.2.7 - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 - '@typescript-eslint/eslint-plugin': - specifier: 5.37.0 - version: 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/parser': - specifier: 5.37.0 - version: 5.37.0(eslint@8.23.1)(typescript@5.2.2) + '@types/styled-components': + specifier: ^5.1.26 + version: 5.1.26 + '@types/testing-library__jest-dom': + specifier: ^5.14.9 + version: 5.14.9 '@vitejs/plugin-react': specifier: ^1.3.2 version: 1.3.2 + '@votingworks/grout-test-utils': + specifier: workspace:* + version: link:../../../libs/grout/test-utils '@votingworks/monorepo-utils': specifier: workspace:* version: link:../../../libs/monorepo-utils + '@votingworks/test-utils': + specifier: workspace:* + version: link:../../../libs/test-utils concurrently: - specifier: ^7.4.0 + specifier: 7.6.0 version: 7.6.0 eslint: - specifier: 8.23.1 - version: 8.23.1 + specifier: 8.51.0 + version: 8.51.0 eslint-config-airbnb: specifier: ^19.0.4 - version: 19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.23.1) + version: 19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.51.0) eslint-config-prettier: - specifier: ^8.5.0 - version: 8.5.0(eslint@8.23.1) + specifier: ^9.0.0 + version: 9.0.0(eslint@8.51.0) eslint-config-react-app: specifier: ^7.0.1 - version: 7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2) + version: 7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2) eslint-import-resolver-node: - specifier: ^0.3.4 + specifier: ^0.3.9 version: 0.3.9 - eslint-plugin-flowtype: - specifier: ^5.2.0 - version: 5.2.0(eslint@8.23.1) eslint-plugin-import: specifier: ^2.26.0 - version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) eslint-plugin-jest: - specifier: ^26.1.5 - version: 26.1.5(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2) + specifier: ^27.2.3 + version: 27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2) eslint-plugin-jsx-a11y: specifier: ^6.6.1 - version: 6.6.1(eslint@8.23.1) + version: 6.6.1(eslint@8.51.0) eslint-plugin-prettier: - specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.5.0)(eslint@8.23.1)(prettier@3.0.3) + specifier: ^5.0.0 + version: 5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3) eslint-plugin-react: specifier: ^7.31.8 - version: 7.31.8(eslint@8.23.1) + version: 7.31.8(eslint@8.51.0) eslint-plugin-react-hooks: specifier: ^4.6.0 - version: 4.6.0(eslint@8.23.1) + version: 4.6.0(eslint@8.51.0) eslint-plugin-testing-library: specifier: ^5.6.4 - version: 5.6.4(eslint@8.23.1)(typescript@5.2.2) + version: 5.6.4(eslint@8.51.0)(typescript@5.2.2) eslint-plugin-vx: specifier: workspace:* version: link:../../../libs/eslint-plugin-vx is-ci-cli: - specifier: ^2.1.2 + specifier: 2.2.0 version: 2.2.0 jest: - specifier: ^29.5.0 + specifier: ^29.6.2 version: 29.6.2(@types/node@16.18.23) jest-environment-jsdom: - specifier: ^29.4.1 + specifier: ^29.6.2 version: 29.6.2 jest-junit: - specifier: ^14.0.1 - version: 14.0.1 + specifier: ^16.0.0 + version: 16.0.0 jest-watch-typeahead: - specifier: ^0.6.4 - version: 0.6.4(jest@29.6.2) + specifier: ^2.2.2 + version: 2.2.2(jest@29.6.2) lint-staged: - specifier: ^10.5.3 - version: 10.5.3 + specifier: 11.0.0 + version: 11.0.0 ts-jest: - specifier: ^29.1.0 - version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2) - typescript: - specifier: 5.2.2 - version: 5.2.2 + specifier: 29.1.1 + version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.18.17)(jest@29.6.2)(typescript@5.2.2) vite: - specifier: ^4.3.9 + specifier: 4.5.0 version: 4.5.0(@types/node@16.18.23) apps/rave-mark/frontend/prodserver: dependencies: express: - specifier: ^4.17.1 + specifier: 4.18.2 version: 4.18.2 http-proxy-middleware: specifier: 1.0.6 version: 1.0.6 - apps/rave-mark/integration-testing: - dependencies: - cypress: - specifier: ^12.13.0 - version: 12.13.0 + apps/rave-scan/backend: devDependencies: - '@testing-library/cypress': - specifier: ^9.0.0 - version: 9.0.0(cypress@12.13.0) - '@typescript-eslint/eslint-plugin': - specifier: 5.37.0 - version: 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/parser': - specifier: 5.37.0 - version: 5.37.0(eslint@8.23.1)(typescript@5.2.2) - eslint: - specifier: 8.23.1 - version: 8.23.1 - eslint-config-airbnb-base: - specifier: ^14.2.1 - version: 14.2.1(eslint-plugin-import@2.26.0)(eslint@8.23.1) - eslint-config-prettier: - specifier: ^8.5.0 - version: 8.5.0(eslint@8.23.1) - eslint-import-resolver-node: - specifier: ^0.3.4 - version: 0.3.9 - eslint-plugin-cypress: - specifier: ^2.12.1 - version: 2.12.1(eslint@8.23.1) - eslint-plugin-flowtype: - specifier: ^5.2.0 - version: 5.2.0(eslint@8.23.1) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - eslint-plugin-jest: - specifier: ^26.1.5 - version: 26.1.5(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2) - eslint-plugin-jsx-a11y: - specifier: ^6.6.1 - version: 6.6.1(eslint@8.23.1) - eslint-plugin-prettier: - specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.5.0)(eslint@8.23.1)(prettier@3.0.3) - eslint-plugin-vx: - specifier: workspace:* - version: link:../../../libs/eslint-plugin-vx is-ci-cli: - specifier: ^2.1.2 + specifier: 2.2.0 version: 2.2.0 - start-server-and-test: - specifier: ^1.12.5 - version: 1.12.5 - typescript: - specifier: 5.2.2 - version: 5.2.2 + + apps/rave-scan/frontend: + dependencies: + tailwindcss: + specifier: ^3.3.3 + version: 3.3.3 + devDependencies: + concurrently: + specifier: 7.6.0 + version: 7.6.0 docs/exercises: dependencies: @@ -886,6 +869,8 @@ importers: specifier: 29.1.1 version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2) + libs/ballot-encoder-rs: {} + libs/basics: dependencies: deep-eql: @@ -984,6 +969,8 @@ importers: specifier: 29.1.1 version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2) + libs/central-scanner: {} + libs/db: dependencies: '@votingworks/basics': @@ -1219,43 +1206,43 @@ importers: dependencies: '@typescript-eslint/eslint-plugin': specifier: 6.7.0 - version: 6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.49.0)(typescript@5.2.2) + version: 6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/utils': specifier: 6.7.0 - version: 6.7.0(eslint@8.49.0)(typescript@5.2.2) + version: 6.7.0(eslint@8.51.0)(typescript@5.2.2) comment-parser: specifier: ^1.4.0 version: 1.4.0 eslint-config-airbnb: specifier: ^19.0.4 - version: 19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.49.0) + version: 19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.51.0) eslint-config-airbnb-base: specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.49.0) + version: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.51.0) eslint-config-prettier: specifier: ^9.0.0 - version: 9.0.0(eslint@8.49.0) + version: 9.0.0(eslint@8.51.0) eslint-import-resolver-node: specifier: ^0.3.9 version: 0.3.9 eslint-import-resolver-typescript: specifier: 3.6.0 - version: 3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.26.0)(eslint@8.49.0) + version: 3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.26.0)(eslint@8.51.0) eslint-plugin-import: specifier: ^2.26.0 - version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + version: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) eslint-plugin-jest: specifier: ^27.2.3 - version: 27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.49.0)(jest@29.6.2)(typescript@5.2.2) + version: 27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2) eslint-plugin-jsx-a11y: specifier: ^6.6.1 - version: 6.6.1(eslint@8.49.0) + version: 6.6.1(eslint@8.51.0) eslint-plugin-prettier: specifier: ^5.0.0 - version: 5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3) + version: 5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3) eslint-plugin-react: specifier: ^7.31.8 - version: 7.31.8(eslint@8.49.0) + version: 7.31.8(eslint@8.51.0) typescript: specifier: 5.2.2 version: 5.2.2 @@ -1271,7 +1258,7 @@ importers: version: 18.2.18 '@typescript-eslint/rule-tester': specifier: ^6.7.0 - version: 6.7.0(@eslint/eslintrc@2.1.2)(eslint@8.49.0)(typescript@5.2.2) + version: 6.7.0(@eslint/eslintrc@2.1.2)(eslint@8.51.0)(typescript@5.2.2) eslint-plugin-vx: specifier: workspace:* version: 'link:' @@ -1854,7 +1841,7 @@ importers: version: 6.0.3 eslint-plugin-storybook: specifier: ^0.6.10 - version: 0.6.10(eslint@8.49.0)(typescript@5.2.2) + version: 0.6.10(eslint@8.51.0)(typescript@5.2.2) eslint-plugin-vx: specifier: workspace:* version: link:../eslint-plugin-vx @@ -1936,34 +1923,34 @@ importers: version: 16.18.23 '@typescript-eslint/eslint-plugin': specifier: 6.7.0 - version: 6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.49.0)(typescript@5.2.2) + version: 6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: 6.7.0 - version: 6.7.0(eslint@8.49.0)(typescript@5.2.2) + version: 6.7.0(eslint@8.51.0)(typescript@5.2.2) esbuild-runner: specifier: 2.2.2 version: 2.2.2(esbuild@0.17.11) eslint: - specifier: 8.49.0 - version: 8.49.0 + specifier: 8.51.0 + version: 8.51.0 eslint-config-prettier: specifier: ^9.0.0 - version: 9.0.0(eslint@8.49.0) + version: 9.0.0(eslint@8.51.0) eslint-import-resolver-node: specifier: ^0.3.9 version: 0.3.9 eslint-import-resolver-typescript: specifier: 3.6.0 - version: 3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.27.5)(eslint@8.49.0) + version: 3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.27.5)(eslint@8.51.0) eslint-plugin-import: specifier: ^2.26.0 - version: 2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + version: 2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) eslint-plugin-jest: specifier: ^27.2.3 - version: 27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.49.0)(jest@29.6.2)(typescript@5.2.2) + version: 27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2) eslint-plugin-prettier: specifier: ^5.0.0 - version: 5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3) + version: 5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3) eslint-plugin-vx: specifier: workspace:* version: link:../eslint-plugin-vx @@ -1991,9 +1978,6 @@ importers: ts-jest: specifier: 29.1.1 version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2) - typescript: - specifier: 5.2.2 - version: 5.2.2 libs/printing: dependencies: @@ -2341,6 +2325,8 @@ importers: specifier: 29.1.1 version: 29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2) + libs/types-rs: {} + libs/ui: dependencies: '@fortawesome/fontawesome-svg-core': @@ -2550,7 +2536,7 @@ importers: version: 2.2.2(esbuild@0.17.11) eslint-plugin-storybook: specifier: ^0.6.10 - version: 0.6.10(eslint@8.49.0)(typescript@5.2.2) + version: 0.6.10(eslint@8.51.0)(typescript@5.2.2) eslint-plugin-vx: specifier: workspace:* version: link:../eslint-plugin-vx @@ -2624,6 +2610,8 @@ importers: specifier: 4.5.0 version: 4.5.0(@types/node@16.18.23) + libs/ui-rs: {} + libs/usb-drive: dependencies: '@votingworks/basics': @@ -2852,6 +2840,12 @@ importers: specifier: 5.2.2 version: 5.2.2 + services/rave-server: + devDependencies: + is-ci-cli: + specifier: 2.2.0 + version: 2.2.0 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -2862,6 +2856,11 @@ packages: resolution: {integrity: sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==} dev: true + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: false + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -2935,7 +2934,7 @@ packages: transitivePeerDependencies: - supports-color - /@babel/eslint-parser@7.22.15(@babel/core@7.22.9)(eslint@8.23.1): + /@babel/eslint-parser@7.22.15(@babel/core@7.22.9)(eslint@8.51.0): resolution: {integrity: sha512-yc8OOBIQk1EcRrpizuARSQS0TWAcOMpEJ1aafhNznaeYkeL+OhqnDObGFylB8ka8VFF/sZc+S4RzHyO+3LjQxg==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -2944,7 +2943,7 @@ packages: dependencies: '@babel/core': 7.22.9 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.23.1 + eslint: 8.51.0 eslint-visitor-keys: 2.1.0 semver: 6.3.1 dev: true @@ -3138,7 +3137,7 @@ packages: '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 - resolve: 1.18.1 + resolve: 1.22.4 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -3154,7 +3153,7 @@ packages: '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 - resolve: 1.18.1 + resolve: 1.22.4 transitivePeerDependencies: - supports-color dev: true @@ -3169,7 +3168,7 @@ packages: '@babel/helper-plugin-utils': 7.22.5 debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 - resolve: 1.18.1 + resolve: 1.22.4 transitivePeerDependencies: - supports-color dev: true @@ -5494,7 +5493,7 @@ packages: '@codemod/parser': 1.2.1 is-ci-cli: 2.2.0 recast: 0.19.1 - resolve: 1.18.1 + resolve: 1.22.4 transitivePeerDependencies: - supports-color dev: true @@ -5509,6 +5508,7 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} requiresBuild: true + dev: true optional: true /@csstools/css-parser-algorithms@2.3.1(@csstools/css-tokenizer@2.2.0): @@ -5545,37 +5545,6 @@ packages: postcss-selector-parser: 6.0.13 dev: true - /@cypress/request@2.88.12: - resolution: {integrity: sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==} - engines: {node: '>= 6'} - dependencies: - aws-sign2: 0.7.0 - aws4: 1.12.0 - caseless: 0.12.0 - combined-stream: 1.0.8 - extend: 3.0.2 - forever-agent: 0.6.1 - form-data: 2.3.3 - http-signature: 1.3.6 - is-typedarray: 1.0.0 - isstream: 0.1.2 - json-stringify-safe: 5.0.1 - mime-types: 2.1.35 - performance-now: 2.1.0 - qs: 6.10.4 - safe-buffer: 5.2.1 - tough-cookie: 4.1.3 - tunnel-agent: 0.6.0 - uuid: 8.3.2 - - /@cypress/xvfb@1.2.4(supports-color@8.1.1): - resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} - dependencies: - debug: 3.2.7(supports-color@8.1.1) - lodash.once: 4.1.1 - transitivePeerDependencies: - - supports-color - /@discoveryjs/json-ext@0.5.7: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -6062,46 +6031,29 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.23.1): + /@eslint-community/eslint-utils@4.4.0(eslint@8.49.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.23.1 + eslint: 8.49.0 eslint-visitor-keys: 3.4.3 dev: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.49.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.51.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.49.0 + eslint: 8.51.0 eslint-visitor-keys: 3.4.3 /@eslint-community/regexpp@4.8.1: resolution: {integrity: sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - /@eslint/eslintrc@1.4.1: - resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4(supports-color@5.5.0) - espree: 9.6.1 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - /@eslint/eslintrc@2.1.2: resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -6121,6 +6073,11 @@ packages: /@eslint/js@8.49.0: resolution: {integrity: sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@eslint/js@8.51.0: + resolution: {integrity: sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} /@fal-works/esbuild-plugin-global-externals@2.1.2: resolution: {integrity: sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==} @@ -6223,27 +6180,6 @@ packages: react: 18.2.0 dev: false - /@hapi/hoek@9.3.0: - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - dev: true - - /@hapi/topo@5.1.0: - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - dependencies: - '@hapi/hoek': 9.3.0 - dev: true - - /@humanwhocodes/config-array@0.10.7: - resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4(supports-color@5.5.0) - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /@humanwhocodes/config-array@0.11.11: resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} engines: {node: '>=10.10.0'} @@ -6254,10 +6190,6 @@ packages: transitivePeerDependencies: - supports-color - /@humanwhocodes/gitignore-to-minimatch@1.0.2: - resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} - dev: true - /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -6279,7 +6211,6 @@ packages: strip-ansi-cjs: /strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: true /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} @@ -6295,18 +6226,6 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} - /@jest/console@27.5.1: - resolution: {integrity: sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@types/node': 16.18.23 - chalk: 4.1.2 - jest-message-util: 27.5.1 - jest-util: 27.5.1 - slash: 3.0.0 - dev: true - /@jest/console@29.6.2: resolution: {integrity: sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6456,16 +6375,6 @@ packages: callsites: 3.1.0 graceful-fs: 4.2.11 - /@jest/test-result@27.5.1: - resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/console': 27.5.1 - '@jest/types': 27.5.1 - '@types/istanbul-lib-coverage': 2.0.4 - collect-v8-coverage: 1.0.2 - dev: true - /@jest/test-result@29.6.2: resolution: {integrity: sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6641,7 +6550,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} requiresBuild: true - dev: true optional: true /@pkgr/utils@2.4.2: @@ -6655,6 +6563,22 @@ packages: picocolors: 1.0.0 tslib: 2.6.2 + /@puppeteer/browsers@1.7.0: + resolution: {integrity: sha512-sl7zI0IkbQGak/+IE3VEEZab5SSOlI5F6558WvzWGC1n3+C722rfewC1ZIkcF9dsoGSsxhsONoseVlNQG4wWvQ==} + engines: {node: '>=16.3.0'} + hasBin: true + dependencies: + debug: 4.3.4(supports-color@5.5.0) + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.3.0 + tar-fs: 3.0.4 + unbzip2-stream: 1.4.3 + yargs: 17.7.1 + transitivePeerDependencies: + - supports-color + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -7083,20 +7007,6 @@ packages: resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==} dev: true - /@sideway/address@4.1.5: - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - dependencies: - '@hapi/hoek': 9.3.0 - dev: true - - /@sideway/formula@3.0.1: - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - dev: true - - /@sideway/pinpoint@2.0.0: - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - dev: true - /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -8094,35 +8004,6 @@ packages: file-system-cache: 2.3.0 dev: true - /@stylelint/postcss-css-in-js@0.37.3(postcss-syntax@0.36.2)(postcss@7.0.39): - resolution: {integrity: sha512-scLk3cSH1H9KggSniseb2KNAU5D9FWc3H7BxCSAIdtU9OWIyw0zkEZ9qEKHryRM+SExYXRKNb7tOOVNAsQ3iwg==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - peerDependencies: - postcss: '>=7.0.0' - postcss-syntax: '>=0.36.2' - dependencies: - '@babel/core': 7.22.9 - postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-less@3.1.4)(postcss-scss@2.1.1)(postcss@7.0.39) - transitivePeerDependencies: - - supports-color - dev: true - - /@stylelint/postcss-markdown@0.36.2(postcss-syntax@0.36.2)(postcss@7.0.39): - resolution: {integrity: sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==} - deprecated: 'Use the original unforked package instead: postcss-markdown' - peerDependencies: - postcss: '>=7.0.0' - postcss-syntax: '>=0.36.2' - dependencies: - postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-less@3.1.4)(postcss-scss@2.1.1)(postcss@7.0.39) - remark: 13.0.0 - unist-util-find-all-after: 3.0.2 - transitivePeerDependencies: - - supports-color - dev: true - /@szmarczak/http-timer@4.0.5: resolution: {integrity: sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==} engines: {node: '>=10'} @@ -8155,24 +8036,6 @@ packages: use-sync-external-store: 1.2.0(react@18.2.0) dev: false - /@tanstack/react-query@4.32.1(react-dom@17.0.1)(react@17.0.1): - resolution: {integrity: sha512-lPTfOq6bR6DorPaS018gTMd3zs8r06tlERiVY6BRP9SnDkkl4ckqeANava/jPLWrSZP+EA15loQUTmvZs6k2GA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - dependencies: - '@tanstack/query-core': 4.32.1 - react: 17.0.1 - react-dom: 17.0.1(react@17.0.1) - use-sync-external-store: 1.2.0(react@17.0.1) - dev: false - /@tanstack/react-query@4.32.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-lPTfOq6bR6DorPaS018gTMd3zs8r06tlERiVY6BRP9SnDkkl4ckqeANava/jPLWrSZP+EA15loQUTmvZs6k2GA==} peerDependencies: @@ -8190,37 +8053,12 @@ packages: react-dom: 18.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) - /@testing-library/cypress@9.0.0(cypress@12.13.0): - resolution: {integrity: sha512-c1XiCGeHGGTWn0LAU12sFUfoX3qfId5gcSE2yHode+vsyHDWraxDPALjVnHd4/Fa3j4KBcc5k++Ccy6A9qnkMA==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - cypress: ^12.0.0 - dependencies: - '@babel/runtime': 7.22.6 - '@testing-library/dom': 8.20.1 - cypress: 12.13.0 - dev: true - - /@testing-library/dom@8.20.1: - resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} - engines: {node: '>=12'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/runtime': 7.22.6 - '@types/aria-query': 5.0.1 - aria-query: 5.1.3 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - dev: true - /@testing-library/dom@9.3.1: resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.22.5 - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.23.9 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -8233,7 +8071,7 @@ packages: engines: {node: '>=8', npm: '>=6', yarn: '>=1'} dependencies: '@adobe/css-tools': 4.2.0 - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.23.9 '@types/testing-library__jest-dom': 5.14.9 aria-query: 5.3.0 chalk: 3.0.0 @@ -8243,20 +8081,6 @@ packages: redent: 3.0.0 dev: true - /@testing-library/react@12.1.5(react-dom@17.0.1)(react@17.0.1): - resolution: {integrity: sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==} - engines: {node: '>=12'} - peerDependencies: - react: <18.0.0 - react-dom: <18.0.0 - dependencies: - '@babel/runtime': 7.22.6 - '@testing-library/dom': 8.20.1 - '@types/react-dom': 17.0.25 - react: 17.0.1 - react-dom: 17.0.1(react@17.0.1) - dev: true - /@testing-library/react@14.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==} engines: {node: '>=14'} @@ -8264,7 +8088,7 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.23.9 '@testing-library/dom': 9.3.1 '@types/react-dom': 18.2.7 react: 18.2.0 @@ -8276,7 +8100,7 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.23.9 '@testing-library/dom': 9.3.1 dev: true @@ -8284,6 +8108,10 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + /@tootallnate/quickjs-emscripten@0.23.0: + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + dev: false + /@types/aria-query@5.0.1: resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} @@ -8593,12 +8421,6 @@ packages: resolution: {integrity: sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==} dev: true - /@types/mdast@3.0.15: - resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} - dependencies: - '@types/unist': 2.0.3 - dev: true - /@types/mdx@2.0.3: resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==} dev: true @@ -8646,9 +8468,6 @@ packages: form-data: 3.0.1 dev: true - /@types/node@14.18.63: - resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - /@types/node@16.18.23: resolution: {integrity: sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==} @@ -8694,12 +8513,6 @@ packages: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} dev: true - /@types/react-dom@17.0.25: - resolution: {integrity: sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==} - dependencies: - '@types/react': 17.0.39 - dev: true - /@types/react-dom@18.2.7: resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} dependencies: @@ -8744,14 +8557,6 @@ packages: '@types/react': 18.2.18 dev: false - /@types/react@17.0.39: - resolution: {integrity: sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==} - dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.3 - csstype: 3.1.2 - dev: true - /@types/react@18.2.18: resolution: {integrity: sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==} dependencies: @@ -8785,12 +8590,6 @@ packages: '@types/node': 16.18.23 dev: true - /@types/sinonjs__fake-timers@8.1.1: - resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} - - /@types/sizzle@2.3.8: - resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==} - /@types/stack-utils@2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} @@ -8852,6 +8651,12 @@ packages: resolution: {integrity: sha512-cSjhgrr8g4KbPnnijAr/KJDNKa/bBa+ixYkywFRvrhvi9n1WEl7yYbtRyzE6jqNQiSxxJxoAW3STaOQwJHndaw==} dev: false + /@types/xml@1.0.9: + resolution: {integrity: sha512-Wo71d7qF1TPcjbg251MS4m4MeK8F8/+ND2t6qTnwoK+jVmD9k9nGN1g9ocu4pRzLSjKMkbnowb88FFPnt+6Wiw==} + dependencies: + '@types/node': 16.18.23 + dev: true + /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -8871,9 +8676,10 @@ packages: requiresBuild: true dependencies: '@types/node': 16.18.23 + dev: false optional: true - /@typescript-eslint/eslint-plugin@5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8884,12 +8690,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.37.0(eslint@8.23.1)(typescript@5.2.2) + '@typescript-eslint/parser': 5.37.0(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 5.37.0 - '@typescript-eslint/type-utils': 5.37.0(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/utils': 5.37.0(eslint@8.23.1)(typescript@5.2.2) + '@typescript-eslint/type-utils': 5.37.0(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.37.0(eslint@8.51.0)(typescript@5.2.2) debug: 4.3.4(supports-color@5.5.0) - eslint: 8.23.1 + eslint: 8.51.0 functional-red-black-tree: 1.0.1 ignore: 5.2.4 regexpp: 3.2.0 @@ -8900,7 +8706,7 @@ packages: - supports-color dev: true - /@typescript-eslint/eslint-plugin@6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.49.0)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -8912,13 +8718,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.8.1 - '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 6.7.0 - '@typescript-eslint/type-utils': 6.7.0(eslint@8.49.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.7.0(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.0(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.7.0 debug: 4.3.4(supports-color@5.5.0) - eslint: 8.49.0 + eslint: 8.51.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -8928,20 +8734,20 @@ packages: transitivePeerDependencies: - supports-color - /@typescript-eslint/experimental-utils@5.62.0(eslint@8.23.1)(typescript@5.2.2): + /@typescript-eslint/experimental-utils@5.62.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.23.1)(typescript@5.2.2) - eslint: 8.23.1 + '@typescript-eslint/utils': 5.62.0(eslint@8.51.0)(typescript@5.2.2) + eslint: 8.51.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser@5.37.0(eslint@8.23.1)(typescript@5.2.2): + /@typescript-eslint/parser@5.37.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -8955,13 +8761,13 @@ packages: '@typescript-eslint/types': 5.37.0 '@typescript-eslint/typescript-estree': 5.37.0(typescript@5.2.2) debug: 4.3.4(supports-color@5.5.0) - eslint: 8.23.1 + eslint: 8.51.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.7.0(eslint@8.23.1)(typescript@5.2.2): + /@typescript-eslint/parser@6.7.0(eslint@8.49.0)(typescript@5.2.2): resolution: {integrity: sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -8976,13 +8782,13 @@ packages: '@typescript-eslint/typescript-estree': 6.7.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.7.0 debug: 4.3.4(supports-color@5.5.0) - eslint: 8.23.1 + eslint: 8.49.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@6.7.0(eslint@8.49.0)(typescript@5.2.2): + /@typescript-eslint/parser@6.7.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -8997,12 +8803,12 @@ packages: '@typescript-eslint/typescript-estree': 6.7.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.7.0 debug: 4.3.4(supports-color@5.5.0) - eslint: 8.49.0 + eslint: 8.51.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color - /@typescript-eslint/rule-tester@6.7.0(@eslint/eslintrc@2.1.2)(eslint@8.49.0)(typescript@5.2.2): + /@typescript-eslint/rule-tester@6.7.0(@eslint/eslintrc@2.1.2)(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-xOsCbazwq/78/KiJUm2VLVbeoP6XwZtc/gWx8Tz+ajZSv+I9VyX1OnMU0uOj8PY4WHCKUmy8EOyREElAj+2l4w==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -9011,9 +8817,9 @@ packages: dependencies: '@eslint/eslintrc': 2.1.2 '@typescript-eslint/typescript-estree': 6.7.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.0(eslint@8.51.0)(typescript@5.2.2) ajv: 6.12.6 - eslint: 8.49.0 + eslint: 8.51.0 lodash.merge: 4.6.2 semver: 7.5.4 transitivePeerDependencies: @@ -9043,7 +8849,7 @@ packages: '@typescript-eslint/types': 6.7.0 '@typescript-eslint/visitor-keys': 6.7.0 - /@typescript-eslint/type-utils@5.37.0(eslint@8.23.1)(typescript@5.2.2): + /@typescript-eslint/type-utils@5.37.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -9054,16 +8860,16 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.37.0(typescript@5.2.2) - '@typescript-eslint/utils': 5.37.0(eslint@8.23.1)(typescript@5.2.2) + '@typescript-eslint/utils': 5.37.0(eslint@8.51.0)(typescript@5.2.2) debug: 4.3.4(supports-color@5.5.0) - eslint: 8.23.1 + eslint: 8.51.0 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/type-utils@6.7.0(eslint@8.49.0)(typescript@5.2.2): + /@typescript-eslint/type-utils@6.7.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -9074,9 +8880,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 6.7.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.0(eslint@8.51.0)(typescript@5.2.2) debug: 4.3.4(supports-color@5.5.0) - eslint: 8.49.0 + eslint: 8.51.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -9156,80 +8962,59 @@ packages: transitivePeerDependencies: - supports-color - /@typescript-eslint/utils@5.37.0(eslint@8.23.1)(typescript@5.2.2): + /@typescript-eslint/utils@5.37.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@types/json-schema': 7.0.12 - '@typescript-eslint/parser': 6.7.0(eslint@8.23.1)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 5.37.0 '@typescript-eslint/types': 5.37.0 '@typescript-eslint/typescript-estree': 5.37.0(typescript@5.2.2) - eslint: 8.23.1 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.23.1) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/utils@5.62.0(eslint@8.23.1)(typescript@5.2.2): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.23.1) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/parser': 6.7.0(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) - eslint: 8.23.1 + eslint: 8.51.0 eslint-scope: 5.1.1 - semver: 7.5.4 + eslint-utils: 3.0.0(eslint@8.51.0) transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/utils@5.62.0(eslint@8.49.0)(typescript@5.2.2): + /@typescript-eslint/utils@5.62.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.49.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 - '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2) - eslint: 8.49.0 + eslint: 8.51.0 eslint-scope: 5.1.1 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript - /@typescript-eslint/utils@6.7.0(eslint@8.49.0)(typescript@5.2.2): + /@typescript-eslint/utils@6.7.0(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.49.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 - '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 6.7.0 '@typescript-eslint/types': 6.7.0 '@typescript-eslint/typescript-estree': 6.7.0(typescript@5.2.2) - eslint: 8.49.0 + eslint: 8.51.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -9538,6 +9323,15 @@ packages: transitivePeerDependencies: - supports-color + /agent-base@7.1.0: + resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} + engines: {node: '>= 14'} + dependencies: + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false + /aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -9605,11 +9399,6 @@ packages: dependencies: type-fest: 3.13.1 - /ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - dev: true - /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -9637,7 +9426,10 @@ packages: /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false /anymatch@2.0.0: resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} @@ -9675,9 +9467,6 @@ packages: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} dev: false - /arch@2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - /archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -9702,6 +9491,10 @@ packages: readable-stream: 3.6.2 dev: false + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} dependencies: @@ -9810,15 +9603,6 @@ packages: safer-buffer: 2.1.2 dev: false - /asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} - dependencies: - safer-buffer: 2.1.2 - - /assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - /assert@1.5.1: resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} dependencies: @@ -9848,6 +9632,13 @@ packages: engines: {node: '>=4'} dev: true + /ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + dependencies: + tslib: 2.6.2 + dev: false + /ast-types@0.14.2: resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} engines: {node: '>=4'} @@ -9869,11 +9660,6 @@ packages: tslib: 2.6.2 dev: true - /astral-regex@1.0.0: - resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} - engines: {node: '>=4'} - dev: true - /astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -9889,14 +9675,11 @@ packages: /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - /at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - /atob@2.1.2: resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} engines: {node: '>= 4.5.0'} @@ -9911,44 +9694,21 @@ packages: tslib: 2.6.2 dev: false - /autoprefixer@9.8.8: - resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==} - hasBin: true - dependencies: - browserslist: 4.22.1 - caniuse-lite: 1.0.30001547 - normalize-range: 0.1.2 - num2fraction: 1.2.2 - picocolors: 0.2.1 - postcss: 7.0.39 - postcss-value-parser: 4.2.0 - dev: true - /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - /aws-sign2@0.7.0: - resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} - - /aws4@1.12.0: - resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} - /axe-core@4.4.3: resolution: {integrity: sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==} engines: {node: '>=4'} - /axios@0.21.4(debug@4.3.1): - resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} - dependencies: - follow-redirects: 1.14.1(debug@4.3.1) - transitivePeerDependencies: - - debug - dev: true - /axobject-query@2.2.0: resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} + /b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + dev: false + /babel-core@7.0.0-bridge.0(@babel/core@7.22.9): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -10175,17 +9935,13 @@ packages: '@babel/preset-env': 7.22.10(@babel/core@7.22.9) '@babel/preset-react': 7.22.15(@babel/core@7.22.9) '@babel/preset-typescript': 7.16.7(@babel/core@7.22.9) - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.23.9 babel-plugin-macros: 3.1.0 babel-plugin-transform-react-remove-prop-types: 0.4.24 transitivePeerDependencies: - supports-color dev: true - /bail@1.0.5: - resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} - dev: true - /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -10209,10 +9965,10 @@ packages: pascalcase: 0.1.1 dev: false - /bcrypt-pbkdf@1.0.2: - resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - dependencies: - tweetnacl: 0.14.5 + /basic-ftp@5.0.4: + resolution: {integrity: sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==} + engines: {node: '>=10.0.0'} + dev: false /better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} @@ -10261,11 +10017,9 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 - /blob-util@2.0.2: - resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} - /bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: false /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} @@ -10314,7 +10068,6 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: true /braces@2.3.2: resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} @@ -10594,10 +10347,6 @@ packages: responselike: 2.0.0 dev: true - /cachedir@2.4.0: - resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} - engines: {node: '>=6'} - /call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} dependencies: @@ -10608,14 +10357,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - /camelcase-keys@6.2.2: - resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} - engines: {node: '>=8'} - dependencies: - camelcase: 5.3.1 - map-obj: 4.1.0 - quick-lru: 4.0.1 - dev: true + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: false /camelcase-keys@7.0.2: resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} @@ -10654,9 +10399,6 @@ packages: - supports-color dev: false - /caseless@0.12.0: - resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} - /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -10692,22 +10434,6 @@ packages: resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} engines: {node: '>=12.20'} - /character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - dev: true - - /character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - dev: true - - /character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - dev: true - - /check-more-types@2.24.0: - resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} - engines: {node: '>= 0.8.0'} - /cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} dependencies: @@ -10780,6 +10506,15 @@ packages: engines: {node: '>=6.0'} dev: false + /chromium-bidi@0.4.22(devtools-protocol@0.0.1159816): + resolution: {integrity: sha512-wR7Y9Ioez+cNXT4ZP7VNM1HRTljpNnMSLw4/RnwhhZUP4yCU7kIQND00YiktuHekch68jklGPK1q9Jkb29+fQg==} + peerDependencies: + devtools-protocol: '*' + dependencies: + devtools-protocol: 0.0.1159816 + mitt: 3.0.1 + dev: false + /ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -10829,6 +10564,7 @@ packages: string-width: 4.2.3 optionalDependencies: '@colors/colors': 1.5.0 + dev: true /cli-truncate@2.1.0: resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} @@ -10867,13 +10603,6 @@ packages: shallow-clone: 3.0.1 dev: true - /clone-regexp@2.2.0: - resolution: {integrity: sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==} - engines: {node: '>=6'} - dependencies: - is-regexp: 2.1.0 - dev: true - /clone-response@1.0.2: resolution: {integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==} dependencies: @@ -10965,9 +10694,15 @@ packages: /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: false + /commander@6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} + dev: true /commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} @@ -10983,10 +10718,6 @@ packages: engines: {node: '>= 12.0.0'} dev: false - /common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -11151,23 +10882,9 @@ packages: requiresBuild: true dev: true - /core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - /core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - /cosmiconfig@6.0.0: - resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} - engines: {node: '>=8'} - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - dev: true - /cosmiconfig@7.0.1: resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} engines: {node: '>=10'} @@ -11186,7 +10903,6 @@ packages: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - dev: true /crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} @@ -11237,6 +10953,14 @@ packages: transitivePeerDependencies: - encoding + /cross-fetch@4.0.0: + resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -11327,7 +11051,6 @@ packages: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: true /cssom@0.3.8: resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} @@ -11352,63 +11075,13 @@ packages: resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} dev: false - /cypress@12.13.0: - resolution: {integrity: sha512-QJlSmdPk+53Zhy69woJMySZQJoWfEWun3X5OOenGsXjRPVfByVTHorxNehbzhZrEzH9RDUDqVcck0ahtlS+N/Q==} - engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} - hasBin: true - requiresBuild: true - dependencies: - '@cypress/request': 2.88.12 - '@cypress/xvfb': 1.2.4(supports-color@8.1.1) - '@types/node': 14.18.63 - '@types/sinonjs__fake-timers': 8.1.1 - '@types/sizzle': 2.3.8 - arch: 2.2.0 - blob-util: 2.0.2 - bluebird: 3.7.2 - buffer: 5.7.1 - cachedir: 2.4.0 - chalk: 4.1.2 - check-more-types: 2.24.0 - cli-cursor: 3.1.0 - cli-table3: 0.6.2 - commander: 6.2.1 - common-tags: 1.8.2 - dayjs: 1.11.10 - debug: 4.3.4(supports-color@8.1.1) - enquirer: 2.3.6 - eventemitter2: 6.4.7 - execa: 4.1.0 - executable: 4.1.1 - extract-zip: 2.0.1(supports-color@8.1.1) - figures: 3.2.0 - fs-extra: 9.1.0 - getos: 3.2.1 - is-ci: 3.0.1 - is-installed-globally: 0.4.0 - lazy-ass: 1.6.0 - listr2: 3.14.0(enquirer@2.3.6) - lodash: 4.17.21 - log-symbols: 4.1.0 - minimist: 1.2.8 - ospath: 1.2.2 - pretty-bytes: 5.6.0 - proxy-from-env: 1.0.0 - request-progress: 3.0.0 - semver: 7.3.2 - supports-color: 8.1.1 - tmp: 0.2.1 - untildify: 4.0.0 - yauzl: 2.10.0 - /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - /dashdash@1.14.1: - resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} - engines: {node: '>=0.10'} - dependencies: - assert-plus: 1.0.0 + /data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + dev: false /data-urls@3.0.2: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} @@ -11422,9 +11095,6 @@ packages: resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} engines: {node: '>=0.11'} - /dayjs@1.11.10: - resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} - /de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} dev: false @@ -11449,29 +11119,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true - - /debug@3.2.7(supports-color@8.1.1): - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - supports-color: 8.1.1 - - /debug@4.3.1: - resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -11485,18 +11132,6 @@ packages: ms: 2.1.2 supports-color: 5.5.0 - /debug@4.3.4(supports-color@8.1.1): - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - supports-color: 8.1.1 - /decamelize-keys@1.1.0: resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==} engines: {node: '>=0.10.0'} @@ -11665,6 +11300,15 @@ packages: resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} dev: true + /degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + dev: false + /del@6.1.1: resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} engines: {node: '>=10'} @@ -11741,6 +11385,14 @@ packages: - supports-color dev: true + /devtools-protocol@0.0.1159816: + resolution: {integrity: sha512-2cZlHxC5IlgkIWe2pSDmCrDiTzbSJWywjbDDnupOImEBcG31CQgBLV8wWE+5t+C4rimcjHsbzy7CBzf9oFjboA==} + dev: false + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: false + /diff-sequences@29.4.3: resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11759,6 +11411,10 @@ packages: dependencies: path-type: 4.0.0 + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: false + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -11781,13 +11437,6 @@ packages: csstype: 3.1.2 dev: false - /dom-serializer@0.2.2: - resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} - dependencies: - domelementtype: 2.3.0 - entities: 2.2.0 - dev: true - /dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} dependencies: @@ -11801,12 +11450,9 @@ packages: engines: {node: '>=0.4', npm: '>=1.2'} dev: false - /domelementtype@1.3.1: - resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} - dev: true - /domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false /domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} @@ -11814,12 +11460,6 @@ packages: dependencies: webidl-conversions: 7.0.0 - /domhandler@2.4.2: - resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} - dependencies: - domelementtype: 1.3.1 - dev: true - /domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -11831,13 +11471,6 @@ packages: resolution: {integrity: sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==} dev: false - /domutils@1.7.0: - resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} - dependencies: - dom-serializer: 0.2.2 - domelementtype: 1.3.1 - dev: true - /domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} dependencies: @@ -11863,10 +11496,6 @@ packages: resolution: {integrity: sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=} dev: true - /duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - dev: true - /duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} dependencies: @@ -11877,13 +11506,6 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true - - /ecc-jsbn@0.1.2: - resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} - dependencies: - jsbn: 0.1.1 - safer-buffer: 2.1.2 /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -11915,10 +11537,6 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - /emoji-regex@7.0.3: - resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} - dev: true - /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -11965,14 +11583,6 @@ packages: resolution: {integrity: sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==} dev: false - /entities@1.1.2: - resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} - dev: true - - /entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - dev: true - /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -12378,21 +11988,7 @@ packages: optionalDependencies: source-map: 0.6.1 - /eslint-config-airbnb-base@14.2.1(eslint-plugin-import@2.26.0)(eslint@8.23.1): - resolution: {integrity: sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==} - engines: {node: '>= 6'} - peerDependencies: - eslint: ^5.16.0 || ^6.8.0 || ^7.2.0 - eslint-plugin-import: ^2.22.1 - dependencies: - confusing-browser-globals: 1.0.11 - eslint: 8.23.1 - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - object.assign: 4.1.4 - object.entries: 1.1.5 - dev: true - - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.26.0)(eslint@8.23.1): + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.26.0)(eslint@8.51.0): resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -12400,49 +11996,13 @@ packages: eslint-plugin-import: ^2.25.2 dependencies: confusing-browser-globals: 1.0.11 - eslint: 8.23.1 - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - object.assign: 4.1.4 - object.entries: 1.1.5 - semver: 6.3.1 - dev: true - - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.26.0)(eslint@8.49.0): - resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} - engines: {node: ^10.12.0 || >=12.0.0} - peerDependencies: - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.2 - dependencies: - confusing-browser-globals: 1.0.11 - eslint: 8.49.0 - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + eslint: 8.51.0 + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) object.assign: 4.1.4 object.entries: 1.1.5 semver: 6.3.1 - dev: false - - /eslint-config-airbnb@19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.23.1): - resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} - engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.3 - eslint-plugin-jsx-a11y: ^6.5.1 - eslint-plugin-react: ^7.28.0 - eslint-plugin-react-hooks: ^4.3.0 - dependencies: - eslint: 8.23.1 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.23.1) - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - eslint-plugin-jsx-a11y: 6.6.1(eslint@8.23.1) - eslint-plugin-react: 7.31.8(eslint@8.23.1) - eslint-plugin-react-hooks: 4.6.0(eslint@8.23.1) - object.assign: 4.1.4 - object.entries: 1.1.5 - dev: true - /eslint-config-airbnb@19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.49.0): + /eslint-config-airbnb@19.0.4(eslint-plugin-import@2.26.0)(eslint-plugin-jsx-a11y@6.6.1)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.8)(eslint@8.51.0): resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -12452,34 +12012,24 @@ packages: eslint-plugin-react: ^7.28.0 eslint-plugin-react-hooks: ^4.3.0 dependencies: - eslint: 8.49.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.49.0) - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - eslint-plugin-jsx-a11y: 6.6.1(eslint@8.49.0) - eslint-plugin-react: 7.31.8(eslint@8.49.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.49.0) + eslint: 8.51.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.51.0) + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) + eslint-plugin-jsx-a11y: 6.6.1(eslint@8.51.0) + eslint-plugin-react: 7.31.8(eslint@8.51.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.51.0) object.assign: 4.1.4 object.entries: 1.1.5 - dev: false - /eslint-config-prettier@8.5.0(eslint@8.23.1): - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.23.1 - dev: true - - /eslint-config-prettier@9.0.0(eslint@8.49.0): + /eslint-config-prettier@9.0.0(eslint@8.51.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.49.0 + eslint: 8.51.0 - /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2): + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -12490,20 +12040,20 @@ packages: optional: true dependencies: '@babel/core': 7.22.9 - '@babel/eslint-parser': 7.22.15(@babel/core@7.22.9)(eslint@8.23.1) + '@babel/eslint-parser': 7.22.15(@babel/core@7.22.9)(eslint@8.51.0) '@rushstack/eslint-patch': 1.5.1 - '@typescript-eslint/eslint-plugin': 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/parser': 5.37.0(eslint@8.23.1)(typescript@5.2.2) + '@typescript-eslint/eslint-plugin': 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/parser': 5.37.0(eslint@8.51.0)(typescript@5.2.2) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 8.23.1 - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.23.1) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2) - eslint-plugin-jsx-a11y: 6.6.1(eslint@8.23.1) - eslint-plugin-react: 7.31.8(eslint@8.23.1) - eslint-plugin-react-hooks: 4.6.0(eslint@8.23.1) - eslint-plugin-testing-library: 5.6.4(eslint@8.23.1)(typescript@5.2.2) + eslint: 8.51.0 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.51.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2) + eslint-plugin-jsx-a11y: 6.6.1(eslint@8.51.0) + eslint-plugin-react: 7.31.8(eslint@8.51.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.51.0) + eslint-plugin-testing-library: 5.6.4(eslint@8.51.0)(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - '@babel/plugin-syntax-flow' @@ -12517,13 +12067,13 @@ packages: /eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.0 resolve: 1.22.4 transitivePeerDependencies: - supports-color - /eslint-import-resolver-typescript@3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.26.0)(eslint@8.49.0): + /eslint-import-resolver-typescript@3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.26.0)(eslint@8.51.0): resolution: {integrity: sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -12532,9 +12082,9 @@ packages: dependencies: debug: 4.3.4(supports-color@5.5.0) enhanced-resolve: 5.15.0 - eslint: 8.49.0 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + eslint: 8.51.0 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) fast-glob: 3.3.1 get-tsconfig: 4.7.0 is-core-module: 2.13.0 @@ -12545,7 +12095,7 @@ packages: - eslint-import-resolver-webpack - supports-color - /eslint-import-resolver-typescript@3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.27.5)(eslint@8.49.0): + /eslint-import-resolver-typescript@3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.27.5)(eslint@8.51.0): resolution: {integrity: sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -12554,9 +12104,9 @@ packages: dependencies: debug: 4.3.4(supports-color@5.5.0) enhanced-resolve: 5.15.0 - eslint: 8.49.0 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + eslint: 8.51.0 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) fast-glob: 3.3.1 get-tsconfig: 4.7.0 is-core-module: 2.13.0 @@ -12568,7 +12118,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): + /eslint-module-utils@2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0): resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -12589,35 +12139,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) - debug: 3.2.7(supports-color@8.1.1) - eslint: 8.49.0 + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) + debug: 3.2.7(supports-color@5.5.0) + eslint: 8.51.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.26.0)(eslint@8.49.0) + eslint-import-resolver-typescript: 3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.26.0)(eslint@8.51.0) transitivePeerDependencies: - supports-color - /eslint-plugin-cypress@2.12.1(eslint@8.23.1): - resolution: {integrity: sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==} - peerDependencies: - eslint: '>= 3.2.1' - dependencies: - eslint: 8.23.1 - globals: 11.12.0 - dev: true - - /eslint-plugin-flowtype@5.2.0(eslint@8.23.1): - resolution: {integrity: sha512-z7ULdTxuhlRJcEe1MVljePXricuPOrsWfScRXFhNzVD5dmTHWjIF57AxD0e7AbEoLSbjSsaA5S+hCg43WvpXJQ==} - engines: {node: ^10.12.0 || >=12.0.0} - peerDependencies: - eslint: ^7.1.0 - dependencies: - eslint: 8.23.1 - lodash: 4.17.21 - string-natural-compare: 3.0.1 - dev: true - - /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.23.1): + /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.22.15)(eslint@8.51.0): resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -12627,12 +12157,12 @@ packages: dependencies: '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.22.9) '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.22.9) - eslint: 8.23.1 + eslint: 8.51.0 lodash: 4.17.21 string-natural-compare: 3.0.1 dev: true - /eslint-plugin-import@2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): + /eslint-plugin-import@2.26.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0): resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} peerDependencies: @@ -12642,14 +12172,14 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) array-includes: 3.1.6 array.prototype.flat: 1.3.1 debug: 2.6.9 doctrine: 2.1.0 - eslint: 8.49.0 + eslint: 8.51.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) has: 1.0.3 is-core-module: 2.13.0 is-glob: 4.0.3 @@ -12662,7 +12192,7 @@ packages: - eslint-import-resolver-webpack - supports-color - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} peerDependencies: @@ -12672,15 +12202,15 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.0(eslint@8.51.0)(typescript@5.2.2) array-includes: 3.1.6 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 - eslint: 8.49.0 + eslint: 8.51.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.51.0) has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -12695,7 +12225,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -12708,38 +12238,16 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.23.1)(typescript@5.2.2) - eslint: 8.23.1 - jest: 29.6.2(@types/node@16.18.23) - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /eslint-plugin-jest@26.1.5(@typescript-eslint/eslint-plugin@5.37.0)(eslint@8.23.1)(jest@29.6.2)(typescript@5.2.2): - resolution: {integrity: sha512-su89aDuljL9bTjEufTXmKUMSFe2kZUL9bi7+woq+C2ukHZordhtfPm4Vg+tdioHBaKf8v3/FXW9uV0ksqhYGFw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - jest: '*' - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true - jest: - optional: true - dependencies: - '@typescript-eslint/eslint-plugin': 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.23.1)(typescript@5.2.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.23.1)(typescript@5.2.2) - eslint: 8.23.1 + '@typescript-eslint/eslint-plugin': 5.37.0(@typescript-eslint/parser@5.37.0)(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.51.0)(typescript@5.2.2) + eslint: 8.51.0 jest: 29.6.2(@types/node@16.18.23) transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.49.0)(jest@29.6.2)(typescript@5.2.2): + /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.7.0)(eslint@8.51.0)(jest@29.6.2)(typescript@5.2.2): resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -12752,15 +12260,15 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.49.0)(typescript@5.2.2) - '@typescript-eslint/utils': 5.62.0(eslint@8.49.0)(typescript@5.2.2) - eslint: 8.49.0 + '@typescript-eslint/eslint-plugin': 6.7.0(@typescript-eslint/parser@6.7.0)(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.62.0(eslint@8.51.0)(typescript@5.2.2) + eslint: 8.51.0 jest: 29.6.2(@types/node@16.18.23) transitivePeerDependencies: - supports-color - typescript - /eslint-plugin-jsx-a11y@6.6.1(eslint@8.23.1): + /eslint-plugin-jsx-a11y@6.6.1(eslint@8.51.0): resolution: {integrity: sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==} engines: {node: '>=4.0'} peerDependencies: @@ -12774,71 +12282,14 @@ packages: axobject-query: 2.2.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.23.1 + eslint: 8.51.0 has: 1.0.3 jsx-ast-utils: 3.3.3 language-tags: 1.0.5 minimatch: 3.1.2 semver: 6.3.1 - dev: true - /eslint-plugin-jsx-a11y@6.6.1(eslint@8.49.0): - resolution: {integrity: sha512-sXgFVNHiWffBq23uiS/JaP6eVR622DqwB4yTzKvGZGcPq6/yZ3WmOZfuBks/vHWo9GaFOqC2ZK4i6+C35knx7Q==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - dependencies: - '@babel/runtime': 7.22.6 - aria-query: 4.2.2 - array-includes: 3.1.6 - ast-types-flow: 0.0.7 - axe-core: 4.4.3 - axobject-query: 2.2.0 - damerau-levenshtein: 1.0.8 - emoji-regex: 9.2.2 - eslint: 8.49.0 - has: 1.0.3 - jsx-ast-utils: 3.3.3 - language-tags: 1.0.5 - minimatch: 3.1.2 - semver: 6.3.1 - dev: false - - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.5.0)(eslint@8.23.1)(prettier@2.6.2): - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: '>=7.28.0' - eslint-config-prettier: '*' - prettier: '>=2.0.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.23.1 - eslint-config-prettier: 8.5.0(eslint@8.23.1) - prettier: 2.6.2 - prettier-linter-helpers: 1.0.0 - dev: true - - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.5.0)(eslint@8.23.1)(prettier@3.0.3): - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: '>=7.28.0' - eslint-config-prettier: '*' - prettier: '>=2.0.0' - peerDependenciesMeta: - eslint-config-prettier: - optional: true - dependencies: - eslint: 8.23.1 - eslint-config-prettier: 8.5.0(eslint@8.23.1) - prettier: 3.0.3 - prettier-linter-helpers: 1.0.0 - dev: true - - /eslint-plugin-prettier@5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3): + /eslint-plugin-prettier@5.0.0(@types/eslint@8.4.1)(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3): resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -12853,31 +12304,21 @@ packages: optional: true dependencies: '@types/eslint': 8.4.1 - eslint: 8.49.0 - eslint-config-prettier: 9.0.0(eslint@8.49.0) + eslint: 8.51.0 + eslint-config-prettier: 9.0.0(eslint@8.51.0) prettier: 3.0.3 prettier-linter-helpers: 1.0.0 synckit: 0.8.5 - /eslint-plugin-react-hooks@4.6.0(eslint@8.23.1): + /eslint-plugin-react-hooks@4.6.0(eslint@8.51.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.23.1 - dev: true + eslint: 8.51.0 - /eslint-plugin-react-hooks@4.6.0(eslint@8.49.0): - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - dependencies: - eslint: 8.49.0 - dev: false - - /eslint-plugin-react@7.31.8(eslint@8.23.1): + /eslint-plugin-react@7.31.8(eslint@8.51.0): resolution: {integrity: sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==} engines: {node: '>=4'} peerDependencies: @@ -12886,7 +12327,7 @@ packages: array-includes: 3.1.6 array.prototype.flatmap: 1.3.1 doctrine: 2.1.0 - eslint: 8.23.1 + eslint: 8.51.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -12898,40 +12339,16 @@ packages: resolve: 2.0.0-next.3 semver: 6.3.1 string.prototype.matchall: 4.0.7 - dev: true - /eslint-plugin-react@7.31.8(eslint@8.49.0): - resolution: {integrity: sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - dependencies: - array-includes: 3.1.6 - array.prototype.flatmap: 1.3.1 - doctrine: 2.1.0 - eslint: 8.49.0 - estraverse: 5.3.0 - jsx-ast-utils: 3.3.3 - minimatch: 3.1.2 - object.entries: 1.1.5 - object.fromentries: 2.0.5 - object.hasown: 1.1.1 - object.values: 1.1.6 - prop-types: 15.8.1 - resolve: 2.0.0-next.3 - semver: 6.3.1 - string.prototype.matchall: 4.0.7 - dev: false - - /eslint-plugin-storybook@0.6.10(eslint@8.49.0)(typescript@5.2.2): + /eslint-plugin-storybook@0.6.10(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-3DKXRey06EhwnTKaG6fgMqGTy4C3z6Ikyv6VVixO5BvaExWQe3yGWIAufrC2Et0OaAMIaMwx9KWjqb/Wq+JxPg==} engines: {node: 12.x || 14.x || >= 16} peerDependencies: eslint: '>=6' dependencies: '@storybook/csf': 0.0.1 - '@typescript-eslint/utils': 5.62.0(eslint@8.49.0)(typescript@5.2.2) - eslint: 8.49.0 + '@typescript-eslint/utils': 5.62.0(eslint@8.51.0)(typescript@5.2.2) + eslint: 8.51.0 requireindex: 1.2.0 ts-dedent: 2.2.0 transitivePeerDependencies: @@ -12939,14 +12356,14 @@ packages: - typescript dev: true - /eslint-plugin-testing-library@5.6.4(eslint@8.23.1)(typescript@5.2.2): + /eslint-plugin-testing-library@5.6.4(eslint@8.51.0)(typescript@5.2.2): resolution: {integrity: sha512-0oW3tC5NNT2WexmJ3848a/utawOymw4ibl3/NkwywndVAz2hT9+ab70imA7ccg3RaScQgMvJT60OL00hpmJvrg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.23.1)(typescript@5.2.2) - eslint: 8.23.1 + '@typescript-eslint/utils': 5.62.0(eslint@8.51.0)(typescript@5.2.2) + eslint: 8.51.0 transitivePeerDependencies: - supports-color - typescript @@ -12974,13 +12391,13 @@ packages: esrecurse: 4.3.0 estraverse: 5.3.0 - /eslint-utils@3.0.0(eslint@8.23.1): + /eslint-utils@3.0.0(eslint@8.51.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.23.1 + eslint: 8.51.0 eslint-visitor-keys: 2.1.0 dev: true @@ -12993,15 +12410,18 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - /eslint@8.23.1: - resolution: {integrity: sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==} + /eslint@8.49.0: + resolution: {integrity: sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint/eslintrc': 1.4.1 - '@humanwhocodes/config-array': 0.10.7 - '@humanwhocodes/gitignore-to-minimatch': 1.0.2 + '@eslint-community/eslint-utils': 4.4.0(eslint@8.49.0) + '@eslint-community/regexpp': 4.8.1 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.49.0 + '@humanwhocodes/config-array': 0.11.11 '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 @@ -13009,7 +12429,6 @@ packages: doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 - eslint-utils: 3.0.0(eslint@8.23.1) eslint-visitor-keys: 3.4.3 espree: 9.6.1 esquery: 1.5.0 @@ -13019,13 +12438,11 @@ packages: find-up: 5.0.0 glob-parent: 6.0.2 globals: 13.20.0 - globby: 11.1.0 - grapheme-splitter: 1.0.4 + graphemer: 1.4.0 ignore: 5.2.4 - import-fresh: 3.3.0 imurmurhash: 0.1.4 is-glob: 4.0.3 - js-sdsl: 4.4.2 + is-path-inside: 3.0.3 js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 @@ -13033,23 +12450,21 @@ packages: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.3 - regexpp: 3.2.0 strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color dev: true - /eslint@8.49.0: - resolution: {integrity: sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==} + /eslint@8.51.0: + resolution: {integrity: sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.49.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@eslint-community/regexpp': 4.8.1 '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.49.0 + '@eslint/js': 8.51.0 '@humanwhocodes/config-array': 0.11.11 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -13147,21 +12562,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - /event-stream@3.3.4: - resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} - dependencies: - duplexer: 0.1.2 - from: 0.1.7 - map-stream: 0.1.0 - pause-stream: 0.0.11 - split: 0.3.3 - stream-combiner: 0.0.4 - through: 2.3.8 - dev: true - - /eventemitter2@6.4.7: - resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} - /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false @@ -13172,24 +12572,10 @@ packages: /evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} - dependencies: - md5.js: 1.3.5 - safe-buffer: 5.2.1 - dev: false - - /execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: false /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} @@ -13219,19 +12605,6 @@ packages: signal-exit: 3.0.7 strip-final-newline: 3.0.0 - /execall@2.0.0: - resolution: {integrity: sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==} - engines: {node: '>=8'} - dependencies: - clone-regexp: 2.2.0 - dev: true - - /executable@4.1.1: - resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} - engines: {node: '>=4'} - dependencies: - pify: 2.3.0 - /exenv@1.2.2: resolution: {integrity: sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=} dev: false @@ -13359,22 +12732,19 @@ packages: - supports-color dev: true - /extract-zip@2.0.1(supports-color@8.1.1): + /extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} hasBin: true dependencies: - debug: 4.3.4(supports-color@8.1.1) + debug: 4.3.4(supports-color@5.5.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: '@types/yauzl': 2.10.1 transitivePeerDependencies: - supports-color - - /extsprintf@1.3.0: - resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} - engines: {'0': node >=0.6.0} + dev: false /fast-check@2.23.2: resolution: {integrity: sha512-ECYuSlp6NLpvOj8eScKsqoz1ihtCpSDuEC2ofdGvgsEu1obHYEGqreJ/iPzkJFy73yoU0kCFea7PHUQDNM0VNg==} @@ -13475,19 +12845,6 @@ packages: deprecated: This module is no longer supported. dev: false - /figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - dependencies: - escape-string-regexp: 1.0.5 - - /file-entry-cache@5.0.1: - resolution: {integrity: sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==} - engines: {node: '>=4'} - dependencies: - flat-cache: 2.0.1 - dev: true - /file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -13583,15 +12940,6 @@ packages: locate-path: 6.0.0 path-exists: 4.0.0 - /flat-cache@2.0.1: - resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==} - engines: {node: '>=4'} - dependencies: - flatted: 2.0.2 - rimraf: 2.6.3 - write: 1.0.3 - dev: true - /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -13599,10 +12947,6 @@ packages: flatted: 3.2.7 rimraf: 3.0.2 - /flatted@2.0.2: - resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==} - dev: true - /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} @@ -13618,7 +12962,7 @@ packages: readable-stream: 2.3.8 dev: false - /follow-redirects@1.14.1(debug@4.3.1): + /follow-redirects@1.14.1: resolution: {integrity: sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==} engines: {node: '>=4.0'} peerDependencies: @@ -13626,8 +12970,7 @@ packages: peerDependenciesMeta: debug: optional: true - dependencies: - debug: 4.3.1 + dev: false /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -13653,18 +12996,6 @@ packages: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 - dev: true - - /forever-agent@0.6.1: - resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - - /form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 /form-data@3.0.1: resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} @@ -13705,10 +13036,6 @@ packages: readable-stream: 2.3.8 dev: false - /from@0.1.7: - resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} - dev: true - /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -13720,6 +13047,15 @@ packages: jsonfile: 6.1.0 universalify: 2.0.0 + /fs-extra@11.2.0: + resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + /fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} @@ -13729,15 +13065,6 @@ packages: universalify: 0.1.2 dev: false - /fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.1.0 - universalify: 2.0.0 - /fs-merger@3.2.1: resolution: {integrity: sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==} dependencies: @@ -13900,11 +13227,6 @@ packages: engines: {node: '>=0.12.0'} dev: true - /get-stdin@7.0.0: - resolution: {integrity: sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==} - engines: {node: '>=8'} - dev: true - /get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -13927,21 +13249,23 @@ packages: dependencies: resolve-pkg-maps: 1.0.0 + /get-uri@6.0.3: + resolution: {integrity: sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==} + engines: {node: '>= 14'} + dependencies: + basic-ftp: 5.0.4 + data-uri-to-buffer: 6.0.2 + debug: 4.3.4(supports-color@5.5.0) + fs-extra: 11.2.0 + transitivePeerDependencies: + - supports-color + dev: false + /get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} dev: false - /getos@3.2.1: - resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} - dependencies: - async: 3.2.4 - - /getpass@0.1.7: - resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} - dependencies: - assert-plus: 1.0.0 - /giget@1.0.0: resolution: {integrity: sha512-KWELZn3Nxq5+0So485poHrFriK9Bn3V/x9y+wgqrHkbmnGbjfLmZ685/SVA/ovW+ewoqW0gVI47pI4yW/VNobQ==} hasBin: true @@ -14017,6 +13341,18 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.3 + path-scurry: 1.10.1 + dev: false + /glob@10.3.3: resolution: {integrity: sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==} engines: {node: '>=16 || 14 >=14.17'} @@ -14039,12 +13375,6 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 - /global-dirs@3.0.1: - resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} - engines: {node: '>=10'} - dependencies: - ini: 2.0.0 - /global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -14120,14 +13450,6 @@ packages: resolution: {integrity: sha512-l+8esYHTKOx2G/Aao4lEQ0bnHWg4fWtJbVoZZT9Knxi01pB8C80BR85nONLFwkkQoFRCmXY+BUcGZN3yZ2QsRA==} dev: true - /gonzales-pe@4.3.0: - resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} - engines: {node: '>=0.6.0'} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: true - /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -14159,10 +13481,6 @@ packages: /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -14374,17 +13692,6 @@ packages: engines: {node: '>=8'} dev: true - /htmlparser2@3.10.1: - resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} - dependencies: - domelementtype: 1.3.1 - domhandler: 2.4.2 - domutils: 1.7.0 - entities: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: true - /htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} dependencies: @@ -14418,6 +13725,16 @@ packages: transitivePeerDependencies: - supports-color + /http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false + /http-proxy-middleware@1.0.6: resolution: {integrity: sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==} engines: {node: '>=8.0.0'} @@ -14436,20 +13753,12 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.14.1(debug@4.3.1) + follow-redirects: 1.14.1 requires-port: 1.0.0 transitivePeerDependencies: - debug dev: false - /http-signature@1.3.6: - resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} - engines: {node: '>=0.10'} - dependencies: - assert-plus: 1.0.0 - jsprim: 2.0.2 - sshpk: 1.18.0 - /https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} dev: false @@ -14473,9 +13782,15 @@ packages: transitivePeerDependencies: - supports-color - /human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} + /https-proxy-agent@7.0.4: + resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} @@ -14523,7 +13838,7 @@ packages: /i18next@22.5.1: resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.23.9 dev: false /i18next@23.5.1: @@ -14618,10 +13933,6 @@ packages: /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - /ini@2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} - /internal-slot@1.0.5: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} @@ -14636,6 +13947,14 @@ packages: loose-envify: 1.4.0 dev: true + /ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + dev: false + /ip@2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} dev: true @@ -14664,17 +13983,6 @@ packages: hasown: 2.0.1 dev: false - /is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - dev: true - - /is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - dev: true - /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -14729,11 +14037,6 @@ packages: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: false - /is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - dev: true - /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -14752,12 +14055,6 @@ packages: dependencies: ci-info: 2.0.0 - /is-ci@3.0.1: - resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} - hasBin: true - dependencies: - ci-info: 3.8.0 - /is-core-module@2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: @@ -14782,10 +14079,6 @@ packages: dependencies: has-tostringtag: 1.0.0 - /is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - dev: true - /is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} dev: true @@ -14836,11 +14129,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - /is-fullwidth-code-point@2.0.0: - resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} - engines: {node: '>=4'} - dev: true - /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -14873,10 +14161,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - dev: true - /is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -14884,13 +14168,6 @@ packages: dependencies: is-docker: 3.0.0 - /is-installed-globally@0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} - dependencies: - global-dirs: 3.0.1 - is-path-inside: 3.0.3 - /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -14991,11 +14268,6 @@ packages: resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} engines: {node: '>=0.10.0'} - /is-regexp@2.1.0: - resolution: {integrity: sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==} - engines: {node: '>=6'} - dev: true - /is-relative@1.0.0: resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} engines: {node: '>=0.10.0'} @@ -15041,9 +14313,6 @@ packages: dependencies: which-typed-array: 1.1.11 - /is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - /is-unc-path@1.0.0: resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} engines: {node: '>=0.10.0'} @@ -15127,9 +14396,6 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - /isstream@0.1.2: - resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} - /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -15180,6 +14446,15 @@ packages: '@pkgjs/parseargs': 0.11.0 dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + /jake@10.8.5: resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} engines: {node: '>=10'} @@ -15404,16 +14679,6 @@ packages: ssim.js: 3.5.0 dev: true - /jest-junit@14.0.1: - resolution: {integrity: sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==} - engines: {node: '>=10.12.0'} - dependencies: - mkdirp: 1.0.4 - strip-ansi: 6.0.1 - uuid: 8.3.2 - xml: 1.0.1 - dev: true - /jest-junit@16.0.0: resolution: {integrity: sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==} engines: {node: '>=10.12.0'} @@ -15439,21 +14704,6 @@ packages: jest-get-type: 29.4.3 pretty-format: 29.6.2 - /jest-message-util@27.5.1: - resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@babel/code-frame': 7.22.5 - '@jest/types': 27.5.1 - '@types/stack-utils': 2.0.1 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.5 - pretty-format: 27.5.1 - slash: 3.0.0 - stack-utils: 2.0.6 - dev: true - /jest-message-util@29.6.2: resolution: {integrity: sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15495,11 +14745,6 @@ packages: dependencies: jest-resolve: 29.6.2 - /jest-regex-util@27.5.1: - resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dev: true - /jest-regex-util@29.4.3: resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15621,18 +14866,6 @@ packages: styled-components: 5.3.11(react-dom@18.2.0)(react-is@18.2.0)(react@18.2.0) dev: true - /jest-util@27.5.1: - resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/types': 27.5.1 - '@types/node': 16.18.23 - chalk: 4.1.2 - ci-info: 3.8.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 - dev: true - /jest-util@29.6.2: resolution: {integrity: sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15655,22 +14888,6 @@ packages: leven: 3.1.0 pretty-format: 29.6.2 - /jest-watch-typeahead@0.6.4(jest@29.6.2): - resolution: {integrity: sha512-tGxriteVJqonyrDj/xZHa0E2glKMiglMLQqISLCjxLUfeueRBh9VoRF2FKQyYO2xOqrWDTg7781zUejx411ZXA==} - engines: {node: '>=10'} - peerDependencies: - jest: ^26.0.0 || ^27.0.0 - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - jest: 29.6.2(@types/node@16.18.23) - jest-regex-util: 27.5.1 - jest-watcher: 27.5.1 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - dev: true - /jest-watch-typeahead@2.2.2(jest@29.6.2): resolution: {integrity: sha512-+QgOFW4o5Xlgd6jGS5X37i08tuuXNW8X0CV9WNFi+3n8ExCIP+E1melYhvYLjv5fE6D0yyzk74vsSO8I6GqtvQ==} engines: {node: ^14.17.0 || ^16.10.0 || >=18.0.0} @@ -15686,19 +14903,6 @@ packages: string-length: 5.0.1 strip-ansi: 7.1.0 - /jest-watcher@27.5.1: - resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - '@jest/test-result': 27.5.1 - '@jest/types': 27.5.1 - '@types/node': 16.18.23 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - jest-util: 27.5.1 - string-length: 4.0.2 - dev: true - /jest-watcher@29.6.2: resolution: {integrity: sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -15741,19 +14945,10 @@ packages: - supports-color - ts-node - /joi@17.12.2: - resolution: {integrity: sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==} - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - dev: true - - /js-sdsl@4.4.2: - resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} - dev: true + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: false /js-sha256@0.9.0: resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==} @@ -15775,8 +14970,9 @@ packages: dependencies: argparse: 2.0.1 - /jsbn@0.1.1: - resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + /jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + dev: false /jscodeshift@0.14.0(@babel/preset-env@7.22.10): resolution: {integrity: sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==} @@ -15878,13 +15074,11 @@ packages: /json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: false /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -15909,15 +15103,6 @@ packages: optionalDependencies: graceful-fs: 4.2.11 - /jsprim@2.0.2: - resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} - engines: {'0': node >=0.6.0} - dependencies: - assert-plus: 1.0.0 - extsprintf: 1.3.0 - json-schema: 0.4.0 - verror: 1.10.0 - /jsx-ast-utils@3.3.3: resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} engines: {node: '>=4.0'} @@ -15962,10 +15147,6 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - /known-css-properties@0.18.0: - resolution: {integrity: sha512-69AgJ1rQa7VvUsd2kpvVq+VeObDuo3zrj0CzM5Slmf6yduQFAI2kXPDQJR2IE/u6MSAUOJrwSzjg5vlz8qcMiw==} - dev: true - /known-css-properties@0.27.0: resolution: {integrity: sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==} dev: true @@ -15978,10 +15159,6 @@ packages: dependencies: language-subtag-registry: 0.3.21 - /lazy-ass@1.6.0: - resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} - engines: {node: '> 0.8'} - /lazy-universal-dotenv@4.0.0: resolution: {integrity: sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==} engines: {node: '>=14.0.0'} @@ -16035,32 +15212,14 @@ packages: engines: {node: '>=10'} dev: false + /lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + engines: {node: '>=14'} + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - /lint-staged@10.5.3: - resolution: {integrity: sha512-TanwFfuqUBLufxCc3RUtFEkFraSPNR3WzWcGF39R3f2J7S9+iF9W0KTVLfSy09lYGmZS5NDCxjNvhGMSJyFCWg==} - hasBin: true - dependencies: - chalk: 4.1.2 - cli-truncate: 2.1.0 - commander: 6.2.1 - cosmiconfig: 7.0.1 - debug: 4.3.4(supports-color@5.5.0) - dedent: 0.7.0 - enquirer: 2.3.6 - execa: 4.1.0 - listr2: 3.14.0(enquirer@2.3.6) - log-symbols: 4.1.0 - micromatch: 4.0.5 - normalize-path: 3.0.0 - please-upgrade-node: 3.2.0 - string-argv: 0.3.1 - stringify-object: 3.3.0 - transitivePeerDependencies: - - supports-color - dev: true - /lint-staged@11.0.0: resolution: {integrity: sha512-3rsRIoyaE8IphSUtO1RVTFl1e0SLBtxxUOPBtHxQgBHS5/i6nqvjcUfNioMa4BU9yGnPzbO+xkfLtXtxBpCzjw==} hasBin: true @@ -16177,9 +15336,6 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - /lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - /lodash.sample@4.2.1: resolution: {integrity: sha512-odCZufa8jYDBZQ+JOSePWRs+iApPdvIp3qAiKc9F22RdSCMLuUu60Jvgs2M6qMisKAeBZoumAkqDiGr9HDym/Q==} dev: false @@ -16211,20 +15367,6 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - /log-symbols@2.2.0: - resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} - engines: {node: '>=4'} - dependencies: - chalk: 2.4.2 - dev: true - - /log-symbols@3.0.0: - resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} - engines: {node: '>=8'} - dependencies: - chalk: 2.4.2 - dev: true - /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -16241,10 +15383,6 @@ packages: slice-ansi: 4.0.0 wrap-ansi: 6.2.0 - /longest-streak@2.0.4: - resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} - dev: true - /loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -16267,7 +15405,6 @@ packages: /lru-cache@10.0.1: resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} engines: {node: 14 || >=16.14} - dev: true /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -16280,6 +15417,11 @@ packages: dependencies: yallist: 4.0.0 + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + /luxon@3.3.0: resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} engines: {node: '>=12'} @@ -16349,10 +15491,6 @@ packages: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} dev: true - /map-stream@0.1.0: - resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} - dev: true - /map-visit@1.0.0: resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} engines: {node: '>=0.10.0'} @@ -16402,37 +15540,10 @@ packages: unist-util-visit: 2.0.3 dev: true - /mdast-util-from-markdown@0.8.5: - resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-string: 2.0.0 - micromark: 2.11.4 - parse-entities: 2.0.0 - unist-util-stringify-position: 2.0.3 - transitivePeerDependencies: - - supports-color - dev: true - - /mdast-util-to-markdown@0.6.5: - resolution: {integrity: sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==} - dependencies: - '@types/unist': 2.0.3 - longest-streak: 2.0.4 - mdast-util-to-string: 2.0.0 - parse-entities: 2.0.0 - repeat-string: 1.6.1 - zwitch: 1.0.5 - dev: true - /mdast-util-to-string@1.1.0: resolution: {integrity: sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==} dev: true - /mdast-util-to-string@2.0.0: - resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} - dev: true - /mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: true @@ -16490,23 +15601,6 @@ packages: yargs-parser: 20.2.9 dev: true - /meow@6.1.1: - resolution: {integrity: sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==} - engines: {node: '>=8'} - dependencies: - '@types/minimist': 1.2.2 - camelcase-keys: 6.2.2 - decamelize-keys: 1.1.0 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 2.5.0 - read-pkg-up: 7.0.1 - redent: 3.0.0 - trim-newlines: 3.0.1 - type-fest: 0.13.1 - yargs-parser: 18.1.3 - dev: true - /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} @@ -16521,15 +15615,6 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} - /micromark@2.11.4: - resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} - dependencies: - debug: 4.3.4(supports-color@5.5.0) - parse-entities: 2.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /micromatch@3.1.10: resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} engines: {node: '>=0.10.0'} @@ -16645,7 +15730,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} dependencies: brace-expansion: 2.0.1 - dev: true /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} @@ -16674,7 +15758,6 @@ packages: /minipass@7.0.3: resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} engines: {node: '>=16 || 14 >=14.17'} - dev: true /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -16699,6 +15782,10 @@ packages: through2: 2.0.5 dev: false + /mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + dev: false + /mixin-deep@1.3.2: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} @@ -16759,6 +15846,14 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + /nan@2.16.0: resolution: {integrity: sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==} requiresBuild: true @@ -16774,7 +15869,6 @@ packages: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: true /nanomatch@1.2.13: resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} @@ -16809,6 +15903,11 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + /netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + dev: false + /node-abi@3.15.0: resolution: {integrity: sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==} engines: {node: '>=10'} @@ -16932,7 +16031,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.18.1 + resolve: 1.22.4 semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -16958,15 +16057,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - /normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - dev: true - - /normalize-selector@0.2.0: - resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==} - dev: true - /normalize-url@6.0.1: resolution: {integrity: sha512-VU4pzAuh7Kip71XEmO9aNREYAdMHFGTVj/i+CaTImS8x0i1d3jUZkXhqluy/PRgjPLMgsLQulYY3PJ/aSbSjpQ==} engines: {node: '>=10'} @@ -17010,10 +16100,6 @@ packages: boolbase: 1.0.0 dev: false - /num2fraction@1.2.2: - resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} - dev: true - /nwsapi@2.2.2: resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} @@ -17030,6 +16116,11 @@ packages: kind-of: 3.2.2 dev: false + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: false + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} @@ -17205,9 +16296,6 @@ packages: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} dev: false - /ospath@1.2.2: - resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} - /p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -17272,6 +16360,30 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /pac-proxy-agent@7.0.1: + resolution: {integrity: sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==} + engines: {node: '>= 14'} + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + get-uri: 6.0.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + dev: false + /pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} dev: true @@ -17310,17 +16422,6 @@ packages: hex-rgb: 4.3.0 dev: true - /parse-entities@2.0.0: - resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - dev: true - /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -17392,7 +16493,6 @@ packages: dependencies: lru-cache: 10.0.1 minipass: 7.0.3 - dev: true /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -17421,12 +16521,6 @@ packages: resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} dev: true - /pause-stream@0.0.11: - resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} - dependencies: - through: 2.3.8 - dev: true - /pbkdf2@3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} @@ -17468,13 +16562,6 @@ packages: /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - /performance-now@2.1.0: - resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} - - /picocolors@0.2.1: - resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} - dev: true - /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -17485,6 +16572,7 @@ packages: /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + dev: false /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -17572,49 +16660,59 @@ packages: engines: {node: '>=0.10.0'} dev: false - /postcss-html@0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39): - resolution: {integrity: sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==} + /postcss-import@15.1.0(postcss@8.4.27): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} peerDependencies: - postcss: '>=5.0.0' - postcss-syntax: '>=0.36.0' + postcss: ^8.0.0 dependencies: - htmlparser2: 3.10.1 - postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-less@3.1.4)(postcss-scss@2.1.1)(postcss@7.0.39) - dev: true + postcss: 8.4.27 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.4 + dev: false - /postcss-less@3.1.4: - resolution: {integrity: sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==} - engines: {node: '>=6.14.4'} + /postcss-js@4.0.1(postcss@8.4.27): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 dependencies: - postcss: 7.0.39 - dev: true + camelcase-css: 2.0.1 + postcss: 8.4.27 + dev: false - /postcss-media-query-parser@0.2.3: - resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} - dev: true + /postcss-load-config@4.0.2(postcss@8.4.27): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.1 + postcss: 8.4.27 + yaml: 2.3.4 + dev: false - /postcss-reporter@6.0.1: - resolution: {integrity: sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==} - engines: {node: '>=6'} + /postcss-nested@6.0.1(postcss@8.4.27): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 dependencies: - chalk: 2.4.2 - lodash: 4.17.21 - log-symbols: 2.2.0 - postcss: 7.0.39 - dev: true + postcss: 8.4.27 + postcss-selector-parser: 6.0.13 + dev: false /postcss-resolve-nested-selector@0.1.1: resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} dev: true - /postcss-safe-parser@4.0.2: - resolution: {integrity: sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==} - engines: {node: '>=6.0.0'} - dependencies: - postcss: 7.0.39 - dev: true - /postcss-safe-parser@6.0.0(postcss@8.4.27): resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} engines: {node: '>=12.0'} @@ -17624,35 +16722,12 @@ packages: postcss: 8.4.27 dev: true - /postcss-sass@0.4.4: - resolution: {integrity: sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==} - dependencies: - gonzales-pe: 4.3.0 - postcss: 7.0.39 - dev: true - - /postcss-scss@2.1.1: - resolution: {integrity: sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==} - engines: {node: '>=6.0.0'} - dependencies: - postcss: 7.0.39 - dev: true - /postcss-selector-parser@6.0.13: resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} engines: {node: '>=4'} dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: true - - /postcss-sorting@4.1.0: - resolution: {integrity: sha512-r4T2oQd1giURJdHQ/RMb72dKZCuLOdWx2B/XhXN1Y1ZdnwXsKH896Qz6vD4tFy9xSjpKNYhlZoJmWyhH/7JUQw==} - engines: {node: '>=6.14.3'} - dependencies: - lodash: 4.17.21 - postcss: 7.0.39 - dev: true /postcss-styled-syntax@0.4.0(postcss@8.4.27)(typescript@5.2.2): resolution: {integrity: sha512-LvG++K8LtIyX1Q1mNuZVQYmBo+SCwn90cEkMigo4/I0QwXrEiYt8nPeJ5rrI5Uuh+5w7hRfPyJKlvQdhVZBhUg==} @@ -17667,44 +16742,9 @@ packages: - typescript dev: true - /postcss-syntax@0.36.2(postcss-html@0.36.0)(postcss-less@3.1.4)(postcss-scss@2.1.1)(postcss@7.0.39): - resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} - peerDependencies: - postcss: '>=5.0.0' - postcss-html: '*' - postcss-jsx: '*' - postcss-less: '*' - postcss-markdown: '*' - postcss-scss: '*' - peerDependenciesMeta: - postcss-html: - optional: true - postcss-jsx: - optional: true - postcss-less: - optional: true - postcss-markdown: - optional: true - postcss-scss: - optional: true - dependencies: - postcss: 7.0.39 - postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39) - postcss-less: 3.1.4 - postcss-scss: 2.1.1 - dev: true - /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - /postcss@7.0.39: - resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} - engines: {node: '>=6.0.0'} - dependencies: - picocolors: 0.2.1 - source-map: 0.6.1 - dev: true - /postcss@8.4.27: resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==} engines: {node: ^10 || ^12 || >=14} @@ -17712,7 +16752,6 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: true /prebuild-install@7.1.1: resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} @@ -17764,10 +16803,6 @@ packages: engines: {node: '>=14'} hasBin: true - /pretty-bytes@5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} - /pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17804,7 +16839,6 @@ packages: /progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - dev: true /promise-inflight@1.0.1(bluebird@3.7.2): resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} @@ -17847,25 +16881,29 @@ packages: forwarded: 0.2.0 ipaddr.js: 1.9.1 - /proxy-from-env@1.0.0: - resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + /proxy-agent@6.3.0: + resolution: {integrity: sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.4 + lru-cache: 7.18.3 + pac-proxy-agent: 7.0.1 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.2 + transitivePeerDependencies: + - supports-color + dev: false /proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: true /prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} dev: false - /ps-tree@1.2.0: - resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} - engines: {node: '>= 0.10'} - hasBin: true - dependencies: - event-stream: 3.3.4 - dev: true - /psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -17931,6 +16969,39 @@ packages: - utf-8-validate dev: true + /puppeteer-core@21.1.1: + resolution: {integrity: sha512-Tlcajcf44zwfa9Sbwv3T8BtaNMJ69wtpHIxwl2NOBTyTK3D1wppQovXTjfw0TDOm3a16eCfQ+5BMi3vRQ4kuAQ==} + engines: {node: '>=16.3.0'} + dependencies: + '@puppeteer/browsers': 1.7.0 + chromium-bidi: 0.4.22(devtools-protocol@0.0.1159816) + cross-fetch: 4.0.0 + debug: 4.3.4(supports-color@5.5.0) + devtools-protocol: 0.0.1159816 + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + + /puppeteer@21.1.1: + resolution: {integrity: sha512-2TLntjGA4qLrI9/8N0UK/5OoZJ2Ue7QgphN2SD+RsaHiha12AEiVyMGsB+i6LY1IoPAtEgYIjblQ7lw3kWDNRw==} + engines: {node: '>=16.3.0'} + deprecated: < 21.5.0 is no longer supported + requiresBuild: true + dependencies: + '@puppeteer/browsers': 1.7.0 + cosmiconfig: 8.2.0 + puppeteer-core: 21.1.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + dev: false + /pure-rand@5.0.1: resolution: {integrity: sha512-ksWccjmXOHU2gJBnH0cK1lSYdvSZ0zLoCMSz/nTGh6hDvCSgcRxDyIcOBD6KNxFz3xhMPm/T267Tbe2JRymKEQ==} @@ -17945,12 +17016,6 @@ packages: react: 18.2.0 dev: false - /qs@6.10.4: - resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.0.4 - /qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -17984,11 +17049,6 @@ packages: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} dev: false - /quick-lru@4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - dev: true - /quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -18079,16 +17139,6 @@ packages: - supports-color dev: true - /react-dom@17.0.1(react@17.0.1): - resolution: {integrity: sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==} - peerDependencies: - react: 17.0.1 - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react: 17.0.1 - scheduler: 0.20.2 - /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: @@ -18236,21 +17286,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /react-router-dom@5.3.4(react@17.0.1): - resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} - peerDependencies: - react: '>=15' - dependencies: - '@babel/runtime': 7.22.6 - history: 4.10.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 17.0.1 - react-router: 5.3.4(react@17.0.1) - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - dev: false - /react-router-dom@5.3.4(react@18.2.0): resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} peerDependencies: @@ -18266,23 +17301,6 @@ packages: tiny-warning: 1.0.3 dev: false - /react-router@5.3.4(react@17.0.1): - resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} - peerDependencies: - react: '>=15' - dependencies: - '@babel/runtime': 7.22.6 - history: 4.10.1 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - path-to-regexp: 1.8.0 - prop-types: 15.8.1 - react: 17.0.1 - react-is: 16.13.1 - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 - dev: false - /react-router@5.3.4(react@18.2.0): resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} peerDependencies: @@ -18351,19 +17369,18 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /react@17.0.1: - resolution: {integrity: sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: false + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -18607,14 +17624,6 @@ packages: unist-util-visit: 2.0.3 dev: true - /remark-parse@9.0.0: - resolution: {integrity: sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==} - dependencies: - mdast-util-from-markdown: 0.8.5 - transitivePeerDependencies: - - supports-color - dev: true - /remark-slug@6.1.0: resolution: {integrity: sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==} dependencies: @@ -18623,22 +17632,6 @@ packages: unist-util-visit: 2.0.3 dev: true - /remark-stringify@9.0.1: - resolution: {integrity: sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==} - dependencies: - mdast-util-to-markdown: 0.6.5 - dev: true - - /remark@13.0.0: - resolution: {integrity: sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==} - dependencies: - remark-parse: 9.0.0 - remark-stringify: 9.0.1 - unified: 9.2.2 - transitivePeerDependencies: - - supports-color - dev: true - /remove-accents@0.4.2: resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==} dev: false @@ -18672,6 +17665,7 @@ packages: /repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} + dev: false /replace-ext@1.0.1: resolution: {integrity: sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==} @@ -18683,11 +17677,6 @@ packages: engines: {node: '>= 10'} dev: false - /request-progress@3.0.0: - resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} - dependencies: - throttleit: 1.0.1 - /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -18852,13 +17841,6 @@ packages: aproba: 1.2.0 dev: false - /rxjs@6.6.7: - resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} - engines: {npm: '>=2.0.0'} - dependencies: - tslib: 1.14.1 - dev: true - /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -18896,12 +17878,6 @@ packages: dependencies: xmlchars: 2.2.0 - /scheduler@0.20.2: - resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -18940,11 +17916,6 @@ packages: hasBin: true dev: true - /semver@7.3.2: - resolution: {integrity: sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==} - engines: {node: '>=10'} - hasBin: true - /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} @@ -19070,7 +18041,6 @@ packages: /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - dev: true /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -19117,15 +18087,6 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - /slice-ansi@2.1.0: - resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} - engines: {node: '>=6'} - dependencies: - ansi-styles: 3.2.1 - astral-regex: 1.0.0 - is-fullwidth-code-point: 2.0.0 - dev: true - /slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -19142,6 +18103,11 @@ packages: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + /snapdragon-node@2.1.1: resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} engines: {node: '>=0.10.0'} @@ -19174,6 +18140,25 @@ packages: - supports-color dev: false + /socks-proxy-agent@8.0.2: + resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} + engines: {node: '>= 14'} + dependencies: + agent-base: 7.1.0 + debug: 4.3.4(supports-color@5.5.0) + socks: 2.8.1 + transitivePeerDependencies: + - supports-color + dev: false + + /socks@2.8.1: + resolution: {integrity: sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + dev: false + /sort-keys@5.0.0: resolution: {integrity: sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==} engines: {node: '>=12'} @@ -19214,7 +18199,6 @@ packages: /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: true /source-map-resolve@0.5.3: resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} @@ -19282,11 +18266,6 @@ packages: resolution: {integrity: sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==} dev: true - /specificity@0.4.1: - resolution: {integrity: sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==} - hasBin: true - dev: true - /split-string@3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} @@ -19294,12 +18273,6 @@ packages: extend-shallow: 3.0.2 dev: false - /split@0.3.3: - resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} - dependencies: - through: 2.3.8 - dev: true - /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -19307,21 +18280,6 @@ packages: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} dev: false - /sshpk@1.18.0: - resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} - engines: {node: '>=0.10.0'} - hasBin: true - dependencies: - asn1: 0.2.6 - assert-plus: 1.0.0 - bcrypt-pbkdf: 1.0.2 - dashdash: 1.14.1 - ecc-jsbn: 0.1.2 - getpass: 0.1.7 - jsbn: 0.1.1 - safer-buffer: 2.1.2 - tweetnacl: 0.14.5 - /ssim.js@3.5.0: resolution: {integrity: sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==} dev: true @@ -19346,22 +18304,6 @@ packages: tslib: 2.6.2 dev: false - /start-server-and-test@1.12.5: - resolution: {integrity: sha512-8Wl0J1xwTDhvWoFeXLIP1VyT9GS5i0XG2440gbMQDNgyCBpb+t2XhahY3ysHIs2g5sDsiom6Iyvh3uQtNrAg5g==} - engines: {node: '>=6'} - hasBin: true - dependencies: - bluebird: 3.7.2 - check-more-types: 2.24.0 - debug: 4.3.1 - execa: 5.1.1 - lazy-ass: 1.6.0 - ps-tree: 1.2.0 - wait-on: 5.3.0(debug@4.3.1) - transitivePeerDependencies: - - supports-color - dev: true - /static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -19420,12 +18362,6 @@ packages: readable-stream: 3.6.0 dev: false - /stream-combiner@0.0.4: - resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} - dependencies: - duplexer: 0.1.2 - dev: true - /stream-each@1.2.3: resolution: {integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==} dependencies: @@ -19481,15 +18417,6 @@ packages: resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} dev: true - /string-width@3.1.0: - resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} - engines: {node: '>=6'} - dependencies: - emoji-regex: 7.0.3 - is-fullwidth-code-point: 2.0.0 - strip-ansi: 5.2.0 - dev: true - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -19505,7 +18432,6 @@ packages: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 strip-ansi: 7.1.0 - dev: true /string.prototype.matchall@4.0.7: resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} @@ -19555,13 +18481,6 @@ packages: is-obj: 1.0.1 is-regexp: 1.0.0 - /strip-ansi@5.2.0: - resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} - engines: {node: '>=6'} - dependencies: - ansi-regex: 4.1.1 - dev: true - /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -19639,28 +18558,6 @@ packages: shallowequal: 1.1.0 supports-color: 5.5.0 - /stylelint-config-palantir@4.0.1(stylelint@13.3.3): - resolution: {integrity: sha512-FzG2MeMfiQEFUz6hqU8P2AdkdZ6a0UetkBMDXK9h0Sm4VZwOdjVrfnwt/X6AADBxsnMsXeDaSolPcDgbPrgaIQ==} - peerDependencies: - stylelint: ^9.8.0 - dependencies: - stylelint: 13.3.3 - stylelint-config-standard: 18.3.0(stylelint@13.3.3) - stylelint-order: 2.2.1(stylelint@13.3.3) - optionalDependencies: - stylelint-scss: 3.21.0(stylelint@13.3.3) - dev: true - - /stylelint-config-prettier@8.0.1(stylelint@13.3.3): - resolution: {integrity: sha512-RcjNW7MUaNVqONhJH4+rtlAE3ow/9SsAM0YWV0Lgu3dbTKdWTa/pQXRdFWgoHWpzUKn+9oBKR5x8JdH+20wmgw==} - engines: {node: '>= 6', npm: '>= 3'} - hasBin: true - peerDependencies: - stylelint: '>=11.0.0' - dependencies: - stylelint: 13.3.3 - dev: true - /stylelint-config-recommended@13.0.0(stylelint@15.10.2): resolution: {integrity: sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -19670,23 +18567,6 @@ packages: stylelint: 15.10.2 dev: true - /stylelint-config-recommended@2.2.0(stylelint@13.3.3): - resolution: {integrity: sha512-bZ+d4RiNEfmoR74KZtCKmsABdBJr4iXRiCso+6LtMJPw5rd/KnxUWTxht7TbafrTJK1YRjNgnN0iVZaJfc3xJA==} - peerDependencies: - stylelint: ^8.3.0 || ^9.0.0 || ^10.0.0 - dependencies: - stylelint: 13.3.3 - dev: true - - /stylelint-config-standard@18.3.0(stylelint@13.3.3): - resolution: {integrity: sha512-Tdc/TFeddjjy64LvjPau9SsfVRexmTFqUhnMBrzz07J4p2dVQtmpncRF/o8yZn8ugA3Ut43E6o1GtjX80TFytw==} - peerDependencies: - stylelint: ^8.3.0 || ^9.0.0 || ^10.0.0 - dependencies: - stylelint: 13.3.3 - stylelint-config-recommended: 2.2.0(stylelint@13.3.3) - dev: true - /stylelint-config-standard@34.0.0(stylelint@15.10.2): resolution: {integrity: sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -19697,107 +18577,6 @@ packages: stylelint-config-recommended: 13.0.0(stylelint@15.10.2) dev: true - /stylelint-config-styled-components@0.1.1: - resolution: {integrity: sha512-z5Xz/9GmvxO6e/DLzBMwkB85zHxEEjN6K7Cj80Bi+o/9vR9eS3GX3E9VuMnX9WLFYulqbqLtTapGGY28JBiy9Q==} - dev: true - - /stylelint-order@2.2.1(stylelint@13.3.3): - resolution: {integrity: sha512-019KBV9j8qp1MfBjJuotse6MgaZqGVtXMc91GU9MsS9Feb+jYUvUU3Z8XiClqPdqJZQ0ryXQJGg3U3PcEjXwfg==} - engines: {node: '>=6'} - peerDependencies: - stylelint: ^9.10.1 || ^10.0.0 - dependencies: - lodash: 4.17.21 - postcss: 7.0.39 - postcss-sorting: 4.1.0 - stylelint: 13.3.3 - dev: true - - /stylelint-processor-styled-components@1.10.0: - resolution: {integrity: sha512-g4HpN9rm0JD0LoHuIOcd/FIjTZCJ0ErQ+dC3VTxp+dSvnkV+MklKCCmCQEdz5K5WxF4vPuzfVgdbSDuPYGZhoA==} - dependencies: - '@babel/parser': 7.22.7 - '@babel/traverse': 7.22.8(supports-color@5.5.0) - micromatch: 4.0.5 - postcss: 7.0.39 - transitivePeerDependencies: - - supports-color - dev: true - - /stylelint-scss@3.21.0(stylelint@13.3.3): - resolution: {integrity: sha512-CMI2wSHL+XVlNExpauy/+DbUcB/oUZLARDtMIXkpV/5yd8nthzylYd1cdHeDMJVBXeYHldsnebUX6MoV5zPW4A==} - engines: {node: '>=8'} - requiresBuild: true - peerDependencies: - stylelint: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 - dependencies: - lodash: 4.17.21 - postcss-media-query-parser: 0.2.3 - postcss-resolve-nested-selector: 0.1.1 - postcss-selector-parser: 6.0.13 - postcss-value-parser: 4.2.0 - stylelint: 13.3.3 - dev: true - optional: true - - /stylelint@13.3.3: - resolution: {integrity: sha512-j8Oio2T1YNiJc6iXDaPYd74Jg4zOa1bByNm/g9/Nvnq4tDPsIjMi46jhRZyPPktGPwjJ5FwcmCqIRlH6PVP8mA==} - hasBin: true - dependencies: - '@stylelint/postcss-css-in-js': 0.37.3(postcss-syntax@0.36.2)(postcss@7.0.39) - '@stylelint/postcss-markdown': 0.36.2(postcss-syntax@0.36.2)(postcss@7.0.39) - autoprefixer: 9.8.8 - balanced-match: 1.0.2 - chalk: 4.1.2 - cosmiconfig: 6.0.0 - debug: 4.3.4(supports-color@5.5.0) - execall: 2.0.0 - file-entry-cache: 5.0.1 - get-stdin: 7.0.0 - global-modules: 2.0.0 - globby: 11.1.0 - globjoin: 0.1.4 - html-tags: 3.3.1 - ignore: 5.2.4 - import-lazy: 4.0.0 - imurmurhash: 0.1.4 - known-css-properties: 0.18.0 - leven: 3.1.0 - lodash: 4.17.21 - log-symbols: 3.0.0 - mathml-tag-names: 2.1.3 - meow: 6.1.1 - micromatch: 4.0.5 - normalize-selector: 0.2.0 - postcss: 7.0.39 - postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39) - postcss-less: 3.1.4 - postcss-media-query-parser: 0.2.3 - postcss-reporter: 6.0.1 - postcss-resolve-nested-selector: 0.1.1 - postcss-safe-parser: 4.0.2 - postcss-sass: 0.4.4 - postcss-scss: 2.1.1 - postcss-selector-parser: 6.0.13 - postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-less@3.1.4)(postcss-scss@2.1.1)(postcss@7.0.39) - postcss-value-parser: 4.2.0 - resolve-from: 5.0.0 - slash: 3.0.0 - specificity: 0.4.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - style-search: 0.1.0 - sugarss: 2.0.0 - svg-tags: 1.0.0 - table: 5.4.6 - v8-compile-cache: 2.4.0 - write-file-atomic: 3.0.3 - transitivePeerDependencies: - - postcss-jsx - - postcss-markdown - - supports-color - dev: true - /stylelint@15.10.2: resolution: {integrity: sha512-UxqSb3hB74g4DTO45QhUHkJMjKKU//lNUAOWyvPBVPZbCknJ5HjOWWZo+UDuhHa9FLeVdHBZXxu43eXkjyIPWg==} engines: {node: ^14.13.1 || >=16.0.0} @@ -19851,11 +18630,19 @@ packages: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} dev: false - /sugarss@2.0.0: - resolution: {integrity: sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==} + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true dependencies: - postcss: 7.0.39 - dev: true + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 10.3.10 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: false /superjson@1.13.3: resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} @@ -19916,16 +18703,6 @@ packages: '@pkgr/utils': 2.4.2 tslib: 2.6.2 - /table@5.4.6: - resolution: {integrity: sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==} - engines: {node: '>=6.0.0'} - dependencies: - ajv: 6.12.6 - lodash: 4.17.21 - slice-ansi: 2.1.0 - string-width: 3.1.0 - dev: true - /table@6.8.1: resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} engines: {node: '>=10.0.0'} @@ -19937,6 +18714,37 @@ packages: strip-ansi: 6.0.1 dev: true + /tailwindcss@3.3.3: + resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.27 + postcss-import: 15.1.0(postcss@8.4.27) + postcss-js: 4.0.1(postcss@8.4.27) + postcss-load-config: 4.0.2(postcss@8.4.27) + postcss-nested: 6.0.1(postcss@8.4.27) + postcss-selector-parser: 6.0.13 + resolve: 1.22.4 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + dev: false + /tapable@1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} @@ -19954,6 +18762,14 @@ packages: pump: 3.0.0 tar-stream: 2.2.0 + /tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + dependencies: + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 3.1.7 + dev: false + /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -19964,6 +18780,14 @@ packages: inherits: 2.0.4 readable-stream: 3.6.2 + /tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.2 + streamx: 2.15.1 + dev: false + /tar@6.1.13: resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} engines: {node: '>=10'} @@ -20050,8 +18874,18 @@ packages: /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - /throttleit@1.0.1: - resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false /through2-filter@3.0.0: resolution: {integrity: sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==} @@ -20216,20 +19050,11 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - /trim-newlines@3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - dev: true - /trim-newlines@4.1.1: resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} engines: {node: '>=12'} dev: true - /trough@1.0.5: - resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} - dev: true - /ts-api-utils@1.0.3(typescript@5.2.2): resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} engines: {node: '>=16.13.0'} @@ -20243,6 +19068,10 @@ packages: engines: {node: '>=6.10'} dev: true + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: false + /ts-jest@29.1.1(@babel/core@7.22.9)(@jest/types@29.6.1)(esbuild@0.17.11)(jest@29.6.2)(typescript@5.2.2): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -20349,9 +19178,7 @@ packages: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: safe-buffer: 5.2.1 - - /tweetnacl@0.14.5: - resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: false /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} @@ -20375,11 +19202,6 @@ packages: engines: {node: '>=8'} dev: true - /type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - dev: true - /type-fest@0.16.0: resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} engines: {node: '>=10'} @@ -20431,12 +19253,6 @@ packages: for-each: 0.3.3 is-typed-array: 1.1.12 - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - dependencies: - is-typedarray: 1.0.0 - dev: true - /typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -20461,6 +19277,13 @@ packages: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 + /unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: false + /unc-path-regex@0.1.2: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} @@ -20500,18 +19323,6 @@ packages: engines: {node: '>=4'} dev: true - /unified@9.2.2: - resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==} - dependencies: - '@types/unist': 2.0.3 - bail: 1.0.5 - extend: 3.0.2 - is-buffer: 2.0.5 - is-plain-obj: 2.1.0 - trough: 1.0.5 - vfile: 4.2.1 - dev: true - /union-value@1.0.1: resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} engines: {node: '>=0.10.0'} @@ -20548,22 +19359,10 @@ packages: crypto-random-string: 2.0.0 dev: true - /unist-util-find-all-after@3.0.2: - resolution: {integrity: sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==} - dependencies: - unist-util-is: 4.0.4 - dev: true - /unist-util-is@4.0.4: resolution: {integrity: sha512-3dF39j/u423v4BBQrk1AQ2Ve1FxY5W3JKwXxVFzBODQ6WEvccguhgp802qQLKSnxPODE6WuRZtV+ohlUg4meBA==} dev: true - /unist-util-stringify-position@2.0.3: - resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} - dependencies: - '@types/unist': 2.0.3 - dev: true - /unist-util-visit-parents@3.1.1: resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} dependencies: @@ -20728,14 +19527,6 @@ packages: tslib: 2.6.2 dev: true - /use-sync-external-store@1.2.0(react@17.0.1): - resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 17.0.1 - dev: false - /use-sync-external-store@1.2.0(react@18.2.0): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -20784,10 +19575,6 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true - /v8-compile-cache@2.4.0: - resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} - dev: true - /v8-to-istanbul@9.1.0: resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} engines: {node: '>=10.12.0'} @@ -20815,30 +19602,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - /verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} - engines: {'0': node >=0.6.0} - dependencies: - assert-plus: 1.0.0 - core-util-is: 1.0.2 - extsprintf: 1.3.0 - - /vfile-message@2.0.4: - resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} - dependencies: - '@types/unist': 2.0.3 - unist-util-stringify-position: 2.0.3 - dev: true - - /vfile@4.2.1: - resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} - dependencies: - '@types/unist': 2.0.3 - is-buffer: 2.0.5 - unist-util-stringify-position: 2.0.3 - vfile-message: 2.0.4 - dev: true - /vinyl-fs@3.0.3: resolution: {integrity: sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==} engines: {node: '>= 0.10'} @@ -20959,20 +19722,6 @@ packages: resolution: {integrity: sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==} dev: true - /wait-on@5.3.0(debug@4.3.1): - resolution: {integrity: sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg==} - engines: {node: '>=8.9.0'} - hasBin: true - dependencies: - axios: 0.21.4(debug@4.3.1) - joi: 17.12.2 - lodash: 4.17.21 - minimist: 1.2.8 - rxjs: 6.6.7 - transitivePeerDependencies: - - debug - dev: true - /walk-sync@2.2.0: resolution: {integrity: sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==} engines: {node: 8.* || >= 10.*} @@ -21230,7 +19979,6 @@ packages: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.1.0 - dev: true /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -21243,15 +19991,6 @@ packages: signal-exit: 3.0.7 dev: true - /write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} - dependencies: - imurmurhash: 0.1.4 - is-typedarray: 1.0.0 - signal-exit: 3.0.7 - typedarray-to-buffer: 3.1.5 - dev: true - /write-file-atomic@4.0.2: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -21267,13 +20006,6 @@ packages: signal-exit: 4.1.0 dev: true - /write@1.0.3: - resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==} - engines: {node: '>=4'} - dependencies: - mkdirp: 0.5.6 - dev: true - /ws@6.2.2: resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==} peerDependencies: @@ -21337,13 +20069,10 @@ packages: engines: {node: '>= 14'} dev: false - /yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - dev: true + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: false /yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} @@ -21401,7 +20130,3 @@ packages: /zod@3.14.4: resolution: {integrity: sha512-U9BFLb2GO34Sfo9IUYp0w3wJLlmcyGoMd75qU9yf+DrdGA4kEx6e+l9KOkAlyUO0PSQzZCa3TR4qVlcmwqSDuw==} dev: false - - /zwitch@1.0.5: - resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} - dev: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d82afa56d6..4b1c9eb7c8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,9 @@ packages: - 'libs/dev-dock/*' - 'libs/hmpb/*' + # include all services + - 'services/*' + # include top-level development project - 'codemods' - 'script' diff --git a/script/rave-setup b/script/rave-setup new file mode 100755 index 0000000000..33cc35afbf --- /dev/null +++ b/script/rave-setup @@ -0,0 +1,251 @@ +#!/usr/bin/env bash + +### Sets up a Debian system for running RAVE apps. ### + +set -euo pipefail + +setup-prereqs() { + echo "📦 Installing prerequisites" + + if [[ "${PATH}" != */usr/local/bin* ]]; then + echo -e "\e[1;31merror:\e[0;31m /usr/local/bin doesn't appear to be in your PATH\e[0m" >&2 + echo -e "" >&2 + echo -e " \e[33mPATH=${PATH}\e[0m" >&2 + echo -e "" >&2 + echo -e "\e[1mhint:\e[0m edit your shell config to include this line:" >&2 + echo -e "" >&2 + echo -e " \e[34mexport PATH=\"\${PATH}:/usr/local/bin\"\e[0m" >&2 + echo -e "" >&2 + echo -e "After you do that, open a new shell and re-run this script." >&2 + exit 1 + fi + + sudo apt-get install \ + build-essential \ + chromium \ + cups-bsd \ + curl \ + git \ + libcairo2-dev \ + libgif-dev \ + libjpeg-dev \ + libpango1.0-dev \ + libpcsclite-dev \ + libpixman-1-dev \ + libpng-dev \ + libsane \ + libssl-dev \ + libx11-dev \ + pcscd \ + --yes \ + --quiet \ + --no-upgrade +} + +setup-node() { + echo "🌳 NodeJS" + + local ARCH=$(arch) + local NODE_VERSION=16.19.1 + local NODE_ARCH= + + local EXISTING_NODE_VERSION=$(node -v 2>/dev/null || true) + + case "${EXISTING_NODE_VERSION}" in + "v${NODE_VERSION}") + echo -e "\e[1m[skip]\e[0m NodeJS ${NODE_VERSION} is already installed" + return + ;; + + "") + ;; + + *) + echo -e "\e[1;31merror:\e[0;31m incorrect NodeJS version\e[0m" >&2 + echo -e "" >&2 + echo -e " \e[1mexisting version = \e[0m${EXISTING_NODE_VERSION}" >&2 + echo -e " \e[1mwanted version = \e[0m${NODE_VERSION}" >&2 + echo -e "" >&2 + echo -e "\e[1mhint:\e[0m remove the 'node' installation at $(which node):" >&2 + exit 1 + ;; + esac + + case "${ARCH}" in + aarch64) + NODE_ARCH=arm64 + ;; + + x86_64) + NODE_ARCH=x64 + ;; + + *) + echo "unsupported architecture: ${ARCH}" >&2 + exit 1 + ;; + esac + + curl -sLo- \ + "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" \ + | sudo tar xz --strip-components 1 -C /usr/local + + if [ "$(node -v)" = "v${NODE_VERSION}" ]; then + echo -e "✅ NodeJS ${NODE_VERSION} installed" + else + echo -e "\e[1;31merror:\e[0;31m NodeJS install failed\e[0m" >&2 + exit 1 + fi +} + +setup-rust() { + echo "🦀 Rust" + + if [ -f ~/.cargo/env ]; then + source ~/.cargo/env + fi + + local EXISTING_RUST_VERSION=$(rustc -V 2>/dev/null || true) + + if [ -n "${EXISTING_RUST_VERSION}" ]; then + echo -e "\e[1m[skip]\e[0m ${EXISTING_RUST_VERSION} is already installed" + else + # install rustup and disable confirmation prompt + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source ~/.cargo/env + + if rustc -V >/dev/null 2>&1; then + echo -e "✅ $(rustc -V)" + else + echo -e "\e[1;31merror:\e[0;31m Rust install failed\e[0m" >&2 + exit 1 + fi + fi + + rustup target add wasm32-unknown-unknown +} + +setup-global-devtools() { + echo "🛠️ Global Devtools" + + if ! which dx >/dev/null 2>&1; then + cargo install --locked dioxus-cli + else + echo -e "\e[1m[skip]\e[0m dioxus-cli is already installed" + fi + + if ! which sqlx >/dev/null 2>&1; then + cargo install --locked sqlx-cli + else + echo -e "\e[1m[skip]\e[0m sqlx-cli is already installed" + fi + + if ! which mprocs >/dev/null 2>&1; then + cargo install --locked mprocs + else + echo -e "\e[1m[skip]\e[0m mprocs is already installed" + fi + + if ! which cargo-watch >/dev/null 2>&1; then + cargo install --locked cargo-watch + else + echo -e "\e[1m[skip]\e[0m cargo-watch is already installed" + fi + + local PNPM_VERSION=8.1.0 + local EXISTING_PNPM_VERSION=$(pnpm --version 2>/dev/null || true) + + if [ "${EXISTING_PNPM_VERSION}" != "${PNPM_VERSION}" ]; then + sudo npm install --global pnpm@${PNPM_VERSION} + else + echo -e "\e[1m[skip]\e[0m pnpm@${PNPM_VERSION} is already installed" + fi +} + +setup-postgresql() { + echo "💾 PostgreSQL Databases" + + sudo apt-get install postgresql --yes --quiet --no-upgrade + + if ! systemctl is-active --quiet postgresql; then + sudo service start postgresql + fi + + local USER_IS_SUPERUSER=$( + cd /tmp + sudo -u postgres \ + psql postgres \ + --tuples-only \ + --quiet \ + -c "select pg_user.usesuper from pg_catalog.pg_user where pg_user.usename = '${USER}' limit 1;" \ + | tr -d '[:space:]' \ + || true + ) + + case "${USER_IS_SUPERUSER}" in + t) + echo -e "\e[1m[skip]\e[0m ${USER} is already a PostgreSQL superuser" + ;; + + f) + echo -e "👤 Altering ${USER} PostgreSQL user to be a superuser" + sudo -u postgres \ + psql postgres \ + --quiet \ + -c "alter user ${USER} with superuser;" + ;; + + "") + echo -e "👤 Creating ${USER} PostgreSQL superuser" + sudo -u postgres \ + createuser --superuser "${USER}" + ;; + + *) + echo -e "\e[1;31merror:\e[0;31m failed to check whether ${USER} is a superuser; unexpected psql output: ${USER_IS_SUPERUSER}\e[0m" + exit 1 + ;; + esac + + # $USER should be a superuser at this point, so create the databases directly + for dbname in rave rave_jx rave_scan; do + local HAS_DB=$(psql postgres \ + --quiet \ + --tuples-only \ + -c "select count(*) from pg_database where datistemplate = false and datname = '${dbname}'" \ + | tr -d '[:space:]' + ) + + if [ "${HAS_DB}" = 0 ]; then + echo -e "➕ creating database '${dbname}'" + createdb "${dbname}" + else + echo -e "\e[1m[skip]\e[0m ${dbname} database already exists" + fi + done + + setup-app-db services/rave-server postgres:rave + setup-app-db apps/rave-jx-terminal/backend postgres:rave_jx + setup-app-db apps/rave-scan/backend postgres:rave_scan +} + +setup-app-db() { + local APP="${1}" + local DATABASE_URL="${2}" + + ( + echo -e "\e[1mmigrate:\e[0m running migrations for ${APP}" + cd "${APP}" + cargo sqlx migrate run \ + --source db/migrations \ + --database-url "${DATABASE_URL}" + ) +} + +setup-prereqs +setup-node +setup-rust +setup-global-devtools +setup-postgresql + +echo -e "\e[32m🎉 Success! Please restart your shell.\e[0m" diff --git a/script/reset-db b/script/reset-db new file mode 100755 index 0000000000..c39a1eb8da --- /dev/null +++ b/script/reset-db @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +( + cd apps/rave-mark/backend + rm -rf dev-workspace +) +( + cd apps/rave-scan/backend + cargo sqlx database reset --source=db/migrations +) +( + cd apps/rave-jx-terminal/backend + cargo sqlx database reset --source=db/migrations +) +( + cd services/rave-server + cargo sqlx database reset --source=db/migrations +) \ No newline at end of file diff --git a/script/src/validate-monorepo/validation/circleci.ts b/script/src/validate-monorepo/validation/circleci.ts index 487ef1bbc9..a10f075251 100644 --- a/script/src/validate-monorepo/validation/circleci.ts +++ b/script/src/validate-monorepo/validation/circleci.ts @@ -1,4 +1,8 @@ -import { CIRCLECI_CONFIG_PATH, PnpmPackageInfo, generateCircleCiConfig } from '@votingworks/monorepo-utils'; +import { + CIRCLECI_CONFIG_PATH, + PnpmPackageInfo, + generateCircleCiConfig, +} from '@votingworks/monorepo-utils'; import { readFileSync } from 'fs'; /** @@ -26,9 +30,12 @@ export interface OutdatedConfig { */ export function* checkConfig( workspacePackages: ReadonlyMap, - rustPackageIds: string[], + rustPackageIds: string[] ): Generator { - const expectedCircleCiConfig = generateCircleCiConfig(workspacePackages, rustPackageIds) + const expectedCircleCiConfig = generateCircleCiConfig( + workspacePackages, + rustPackageIds + ); const actualConfig = readFileSync(CIRCLECI_CONFIG_PATH, 'utf-8'); if (expectedCircleCiConfig !== actualConfig) { diff --git a/services/rave-server/.env b/services/rave-server/.env new file mode 100644 index 0000000000..b256a2a597 --- /dev/null +++ b/services/rave-server/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres:rave diff --git a/services/rave-server/.gitignore b/services/rave-server/.gitignore new file mode 100644 index 0000000000..9b5396135a --- /dev/null +++ b/services/rave-server/.gitignore @@ -0,0 +1 @@ +!script/build \ No newline at end of file diff --git a/services/rave-server/.sqlx/query-088f2a385709494b30d8b9c4a792edb13102141d3ee1ce7ca52023f80ffce385.json b/services/rave-server/.sqlx/query-088f2a385709494b30d8b9c4a792edb13102141d3ee1ce7ca52023f80ffce385.json new file mode 100644 index 0000000000..df9fc45762 --- /dev/null +++ b/services/rave-server/.sqlx/query-088f2a385709494b30d8b9c4a792edb13102141d3ee1ce7ca52023f80ffce385.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n registration_id as \"registration_id: ServerId\",\n cast_vote_record,\n cast_vote_record_signature,\n created_at\n FROM printed_ballots\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "common_access_card_certificate", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "registration_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 7, + "name": "cast_vote_record_signature", + "type_info": "Bytea" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "088f2a385709494b30d8b9c4a792edb13102141d3ee1ce7ca52023f80ffce385" +} diff --git a/services/rave-server/.sqlx/query-0908a3548f4c094b729f06d9645745c79ed939919cf78f46b45f47f92b068430.json b/services/rave-server/.sqlx/query-0908a3548f4c094b729f06d9645745c79ed939919cf78f46b45f47f92b068430.json new file mode 100644 index 0000000000..33f1ad5b06 --- /dev/null +++ b/services/rave-server/.sqlx/query-0908a3548f4c094b729f06d9645745c79ed939919cf78f46b45f47f92b068430.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT created_at\n FROM registrations\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0908a3548f4c094b729f06d9645745c79ed939919cf78f46b45f47f92b068430" +} diff --git a/services/rave-server/.sqlx/query-1166fa780d90967db6362468c31a58ee5a2f9b869c5122be4bfa98100fa8da9e.json b/services/rave-server/.sqlx/query-1166fa780d90967db6362468c31a58ee5a2f9b869c5122be4bfa98100fa8da9e.json new file mode 100644 index 0000000000..5101f979eb --- /dev/null +++ b/services/rave-server/.sqlx/query-1166fa780d90967db6362468c31a58ee5a2f9b869c5122be4bfa98100fa8da9e.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT created_at\n FROM registration_requests\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "1166fa780d90967db6362468c31a58ee5a2f9b869c5122be4bfa98100fa8da9e" +} diff --git a/services/rave-server/.sqlx/query-1ad99291100c5835132b78d0299b95f6055b6ad49f95c746bc268d4be50515da.json b/services/rave-server/.sqlx/query-1ad99291100c5835132b78d0299b95f6055b6ad49f95c746bc268d4be50515da.json new file mode 100644 index 0000000000..b37fb28ac5 --- /dev/null +++ b/services/rave-server/.sqlx/query-1ad99291100c5835132b78d0299b95f6055b6ad49f95c746bc268d4be50515da.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n created_at\n FROM printed_ballots\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "1ad99291100c5835132b78d0299b95f6055b6ad49f95c746bc268d4be50515da" +} diff --git a/services/rave-server/.sqlx/query-3548e96944f55ebcba37b8e7ec9ce7fcc7da3214562a130aab92c688893e13f1.json b/services/rave-server/.sqlx/query-3548e96944f55ebcba37b8e7ec9ce7fcc7da3214562a130aab92c688893e13f1.json new file mode 100644 index 0000000000..61f762c7d0 --- /dev/null +++ b/services/rave-server/.sqlx/query-3548e96944f55ebcba37b8e7ec9ce7fcc7da3214562a130aab92c688893e13f1.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registrations (\n id,\n client_id,\n machine_id,\n common_access_card_id,\n registration_request_id,\n election_id,\n precinct_id,\n ballot_style_id\n )\n VALUES (\n $1, $2, $3, $4,\n (SELECT id FROM registration_requests WHERE client_id = $5),\n (SELECT id FROM elections WHERE client_id = $6),\n $7, $8\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Uuid", + "Uuid", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "3548e96944f55ebcba37b8e7ec9ce7fcc7da3214562a130aab92c688893e13f1" +} diff --git a/services/rave-server/.sqlx/query-41a2ba726f24136aa7905660b200c9f3752f4ad6c791a8f0d20d21bb94411cf1.json b/services/rave-server/.sqlx/query-41a2ba726f24136aa7905660b200c9f3752f4ad6c791a8f0d20d21bb94411cf1.json new file mode 100644 index 0000000000..e64c67175b --- /dev/null +++ b/services/rave-server/.sqlx/query-41a2ba726f24136aa7905660b200c9f3752f4ad6c791a8f0d20d21bb94411cf1.json @@ -0,0 +1,94 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id,\n created_at\n FROM registration_requests\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "given_name", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "family_name", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "address_line_1", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "address_line_2", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "city", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "state", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "postal_code", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "state_id", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "41a2ba726f24136aa7905660b200c9f3752f4ad6c791a8f0d20d21bb94411cf1" +} diff --git a/services/rave-server/.sqlx/query-4da37d9febc69ce401de6664a86a8959d85dfd6b59b32d41ca825f69f0de6d91.json b/services/rave-server/.sqlx/query-4da37d9febc69ce401de6664a86a8959d85dfd6b59b32d41ca825f69f0de6d91.json new file mode 100644 index 0000000000..1792efecd9 --- /dev/null +++ b/services/rave-server/.sqlx/query-4da37d9febc69ce401de6664a86a8959d85dfd6b59b32d41ca825f69f0de6d91.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO admins (machine_id, common_access_card_id)\n VALUES ($1, $2)\n ON CONFLICT (common_access_card_id) DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "4da37d9febc69ce401de6664a86a8959d85dfd6b59b32d41ca825f69f0de6d91" +} diff --git a/services/rave-server/.sqlx/query-67684dc852bf3c5313916385f6aecb89117a92ad9841709ff0643ffc64ccf867.json b/services/rave-server/.sqlx/query-67684dc852bf3c5313916385f6aecb89117a92ad9841709ff0643ffc64ccf867.json new file mode 100644 index 0000000000..f0e2f94e20 --- /dev/null +++ b/services/rave-server/.sqlx/query-67684dc852bf3c5313916385f6aecb89117a92ad9841709ff0643ffc64ccf867.json @@ -0,0 +1,92 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id,\n created_at\n FROM registration_requests\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "given_name", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "family_name", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "address_line_1", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "address_line_2", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "city", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "state", + "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "postal_code", + "type_info": "Varchar" + }, + { + "ordinal": 11, + "name": "state_id", + "type_info": "Varchar" + }, + { + "ordinal": 12, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "67684dc852bf3c5313916385f6aecb89117a92ad9841709ff0643ffc64ccf867" +} diff --git a/services/rave-server/.sqlx/query-77c583ca83f68a91e7ba536df8665dbe785a97c9dcd8107a5a75a90aec57c889.json b/services/rave-server/.sqlx/query-77c583ca83f68a91e7ba536df8665dbe785a97c9dcd8107a5a75a90aec57c889.json new file mode 100644 index 0000000000..a1194629c3 --- /dev/null +++ b/services/rave-server/.sqlx/query-77c583ca83f68a91e7ba536df8665dbe785a97c9dcd8107a5a75a90aec57c889.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n registration_request_id as \"registration_request_id: ServerId\",\n election_id as \"election_id: ServerId\",\n precinct_id,\n ballot_style_id,\n created_at\n FROM registrations\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "registration_request_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "election_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "precinct_id", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "ballot_style_id", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "77c583ca83f68a91e7ba536df8665dbe785a97c9dcd8107a5a75a90aec57c889" +} diff --git a/services/rave-server/.sqlx/query-78b992e6d2f51d7eb96191b53172fc9bcd53e06cc70d84284625187c94d011ff.json b/services/rave-server/.sqlx/query-78b992e6d2f51d7eb96191b53172fc9bcd53e06cc70d84284625187c94d011ff.json new file mode 100644 index 0000000000..f86d87d021 --- /dev/null +++ b/services/rave-server/.sqlx/query-78b992e6d2f51d7eb96191b53172fc9bcd53e06cc70d84284625187c94d011ff.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n election_id as \"election_id: ServerId\",\n cast_vote_record as \"cast_vote_record: _\",\n created_at\n FROM scanned_ballots\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "election_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "cast_vote_record: _", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "78b992e6d2f51d7eb96191b53172fc9bcd53e06cc70d84284625187c94d011ff" +} diff --git a/services/rave-server/.sqlx/query-8c0929b010ff6dcd3b56635f15a27ce9124483a96e9237c2f49dd2a7e52b3474.json b/services/rave-server/.sqlx/query-8c0929b010ff6dcd3b56635f15a27ce9124483a96e9237c2f49dd2a7e52b3474.json new file mode 100644 index 0000000000..ad6e56b68b --- /dev/null +++ b/services/rave-server/.sqlx/query-8c0929b010ff6dcd3b56635f15a27ce9124483a96e9237c2f49dd2a7e52b3474.json @@ -0,0 +1,25 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO registration_requests (\n id,\n client_id,\n machine_id,\n common_access_card_id,\n given_name,\n family_name,\n address_line_1,\n address_line_2,\n city,\n state,\n postal_code,\n state_id\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "8c0929b010ff6dcd3b56635f15a27ce9124483a96e9237c2f49dd2a7e52b3474" +} diff --git a/services/rave-server/.sqlx/query-9bd37a8ad15e621157a5899580f5486643767997cab278cf80d0ff68a9632c20.json b/services/rave-server/.sqlx/query-9bd37a8ad15e621157a5899580f5486643767997cab278cf80d0ff68a9632c20.json new file mode 100644 index 0000000000..45a6a2f5ba --- /dev/null +++ b/services/rave-server/.sqlx/query-9bd37a8ad15e621157a5899580f5486643767997cab278cf80d0ff68a9632c20.json @@ -0,0 +1,70 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n registration_request_id as \"registration_request_id: ServerId\",\n election_id as \"election_id: ServerId\",\n precinct_id,\n ballot_style_id,\n created_at\n FROM registrations\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "registration_request_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "election_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "precinct_id", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "ballot_style_id", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "9bd37a8ad15e621157a5899580f5486643767997cab278cf80d0ff68a9632c20" +} diff --git a/services/rave-server/.sqlx/query-a721d3d9e7500562a5c4dea6f4f46170ca116ab80e8df4f5d3961723aee17c84.json b/services/rave-server/.sqlx/query-a721d3d9e7500562a5c4dea6f4f46170ca116ab80e8df4f5d3961723aee17c84.json new file mode 100644 index 0000000000..d525f63f63 --- /dev/null +++ b/services/rave-server/.sqlx/query-a721d3d9e7500562a5c4dea6f4f46170ca116ab80e8df4f5d3961723aee17c84.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: _\",\n client_id as \"client_id: _\",\n machine_id,\n definition,\n election_hash,\n created_at\n FROM elections\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: _", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "definition", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "election_hash", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a721d3d9e7500562a5c4dea6f4f46170ca116ab80e8df4f5d3961723aee17c84" +} diff --git a/services/rave-server/.sqlx/query-bc48af17fc906f194c3c452a5191c364a917d7844e40b206bf7836f716544740.json b/services/rave-server/.sqlx/query-bc48af17fc906f194c3c452a5191c364a917d7844e40b206bf7836f716544740.json new file mode 100644 index 0000000000..85407da4be --- /dev/null +++ b/services/rave-server/.sqlx/query-bc48af17fc906f194c3c452a5191c364a917d7844e40b206bf7836f716544740.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: _\",\n client_id as \"client_id: _\",\n machine_id,\n definition,\n election_hash,\n created_at\n FROM elections\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: _", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: _", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "definition", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "election_hash", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "bc48af17fc906f194c3c452a5191c364a917d7844e40b206bf7836f716544740" +} diff --git a/services/rave-server/.sqlx/query-c72d6ec45e4ebc48188aee36c4e964fd5858a2fa2649c656d5025e4de6c866ae.json b/services/rave-server/.sqlx/query-c72d6ec45e4ebc48188aee36c4e964fd5858a2fa2649c656d5025e4de6c866ae.json new file mode 100644 index 0000000000..461fb2856c --- /dev/null +++ b/services/rave-server/.sqlx/query-c72d6ec45e4ebc48188aee36c4e964fd5858a2fa2649c656d5025e4de6c866ae.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO scanned_ballots (\n id,\n client_id,\n machine_id,\n election_id,\n cast_vote_record\n )\n VALUES (\n $1, $2, $3,\n (SELECT id FROM elections WHERE client_id = $4),\n $5\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Uuid", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "c72d6ec45e4ebc48188aee36c4e964fd5858a2fa2649c656d5025e4de6c866ae" +} diff --git a/services/rave-server/.sqlx/query-d66db8b2deaf5c944481b903ec5a892ad2820668b3fdb8b54ce44d322d61f544.json b/services/rave-server/.sqlx/query-d66db8b2deaf5c944481b903ec5a892ad2820668b3fdb8b54ce44d322d61f544.json new file mode 100644 index 0000000000..c9933cd554 --- /dev/null +++ b/services/rave-server/.sqlx/query-d66db8b2deaf5c944481b903ec5a892ad2820668b3fdb8b54ce44d322d61f544.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n created_at\n FROM scanned_ballots\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d66db8b2deaf5c944481b903ec5a892ad2820668b3fdb8b54ce44d322d61f544" +} diff --git a/services/rave-server/.sqlx/query-d9d643718503989d188c6c78e4fe4129fbba214f4cfb4001eb19edfc4ab7c2ce.json b/services/rave-server/.sqlx/query-d9d643718503989d188c6c78e4fe4129fbba214f4cfb4001eb19edfc4ab7c2ce.json new file mode 100644 index 0000000000..33ac5f8078 --- /dev/null +++ b/services/rave-server/.sqlx/query-d9d643718503989d188c6c78e4fe4129fbba214f4cfb4001eb19edfc4ab7c2ce.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n machine_id,\n common_access_card_id,\n created_at\n FROM admins\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "d9d643718503989d188c6c78e4fe4129fbba214f4cfb4001eb19edfc4ab7c2ce" +} diff --git a/services/rave-server/.sqlx/query-e2603c8a2dda98dca20f1236bd9cd50f2c1ea4343ff236733172c1cf66916ec2.json b/services/rave-server/.sqlx/query-e2603c8a2dda98dca20f1236bd9cd50f2c1ea4343ff236733172c1cf66916ec2.json new file mode 100644 index 0000000000..e89f34a34b --- /dev/null +++ b/services/rave-server/.sqlx/query-e2603c8a2dda98dca20f1236bd9cd50f2c1ea4343ff236733172c1cf66916ec2.json @@ -0,0 +1,21 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO printed_ballots (\n id,\n client_id,\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n registration_id,\n cast_vote_record,\n cast_vote_record_signature\n )\n VALUES (\n $1, $2, $3, $4, $5,\n (SELECT id FROM registrations WHERE client_id = $6),\n $7, $8\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea", + "Uuid", + "Bytea", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "e2603c8a2dda98dca20f1236bd9cd50f2c1ea4343ff236733172c1cf66916ec2" +} diff --git a/services/rave-server/.sqlx/query-f305266bf89a3cab405a359d5f0ab357bbe118fa193c4ccddb9ac0cd9cafe202.json b/services/rave-server/.sqlx/query-f305266bf89a3cab405a359d5f0ab357bbe118fa193c4ccddb9ac0cd9cafe202.json new file mode 100644 index 0000000000..c97a4aaecd --- /dev/null +++ b/services/rave-server/.sqlx/query-f305266bf89a3cab405a359d5f0ab357bbe118fa193c4ccddb9ac0cd9cafe202.json @@ -0,0 +1,68 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n common_access_card_id,\n common_access_card_certificate,\n registration_id as \"registration_id: ServerId\",\n cast_vote_record,\n cast_vote_record_signature,\n created_at\n FROM printed_ballots\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "common_access_card_id", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "common_access_card_certificate", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "registration_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "cast_vote_record", + "type_info": "Bytea" + }, + { + "ordinal": 7, + "name": "cast_vote_record_signature", + "type_info": "Bytea" + }, + { + "ordinal": 8, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "f305266bf89a3cab405a359d5f0ab357bbe118fa193c4ccddb9ac0cd9cafe202" +} diff --git a/services/rave-server/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json b/services/rave-server/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json new file mode 100644 index 0000000000..1d129dedb6 --- /dev/null +++ b/services/rave-server/.sqlx/query-f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT created_at\n FROM elections\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false + ] + }, + "hash": "f472f6c1413aef8176b26064840908fac5d28f2049658fec150196890eda9fef" +} diff --git a/services/rave-server/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json b/services/rave-server/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json new file mode 100644 index 0000000000..34d300f855 --- /dev/null +++ b/services/rave-server/.sqlx/query-f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO elections (\n id,\n client_id,\n machine_id,\n election_hash,\n definition\n )\n VALUES ($1, $2, $3, $4, $5)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Varchar", + "Varchar", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "f55f83a4d9d8faa471bd3c6f9cb40c2f3d81e6158d7abe32c38f3e86a1347eff" +} diff --git a/services/rave-server/.sqlx/query-fd5bbe94ae688cbba67694013649ea3160d5e13c6c23bbbfdc7f3f32668b225f.json b/services/rave-server/.sqlx/query-fd5bbe94ae688cbba67694013649ea3160d5e13c6c23bbbfdc7f3f32668b225f.json new file mode 100644 index 0000000000..dc221706ab --- /dev/null +++ b/services/rave-server/.sqlx/query-fd5bbe94ae688cbba67694013649ea3160d5e13c6c23bbbfdc7f3f32668b225f.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id as \"id: ServerId\",\n client_id as \"client_id: ClientId\",\n machine_id,\n election_id as \"election_id: ServerId\",\n cast_vote_record as \"cast_vote_record: _\",\n created_at\n FROM scanned_ballots\n WHERE created_at > $1\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "client_id: ClientId", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "machine_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "election_id: ServerId", + "type_info": "Uuid" + }, + { + "ordinal": 4, + "name": "cast_vote_record: _", + "type_info": "Bytea" + }, + { + "ordinal": 5, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "fd5bbe94ae688cbba67694013649ea3160d5e13c6c23bbbfdc7f3f32668b225f" +} diff --git a/services/rave-server/Cargo.toml b/services/rave-server/Cargo.toml new file mode 100644 index 0000000000..b1c5c20a64 --- /dev/null +++ b/services/rave-server/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "rave-server" +version = "0.1.0" +edition = "2021" +default-run = "rave-server" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = { workspace = true } +base64 = { workspace = true } +base64-serde = { workspace = true } +clap = { workspace = true } +color-eyre = { workspace = true } +dotenvy = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tower-http = { workspace = true, features = ["trace"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +types-rs = { workspace = true, features = ["sqlx"] } +uuid = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/services/rave-server/Makefile b/services/rave-server/Makefile new file mode 100644 index 0000000000..d3b8e73e31 --- /dev/null +++ b/services/rave-server/Makefile @@ -0,0 +1,24 @@ +ALL: build + +clean: + @echo "🧹 Cleaning…" + @cargo clean + +build: + @echo "🛠️ Building…" + @cargo build --release + +dist: build + @echo "📦 Packaging application…" + @rm -rf dist && mkdir dist + @cp ../../target/release/rave-server dist/rave-server + @echo "- \e[34;4mdist/rave-server\e[0m: application binary" + +run: + @echo "🚀 Running application in production mode…" + @cd dist && ./rave-server + +reset-db: + @echo "🗑️ Resetting database…" + @cargo sqlx database reset --source db/migrations + @echo "✅ Database reset" \ No newline at end of file diff --git a/services/rave-server/build.rs b/services/rave-server/build.rs new file mode 100644 index 0000000000..7609593841 --- /dev/null +++ b/services/rave-server/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} \ No newline at end of file diff --git a/services/rave-server/db/migrations/20230705182146_init.sql b/services/rave-server/db/migrations/20230705182146_init.sql new file mode 100644 index 0000000000..50e7775112 --- /dev/null +++ b/services/rave-server/db/migrations/20230705182146_init.sql @@ -0,0 +1,87 @@ +create table elections ( + id uuid primary key, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_hash varchar(255) not null, + definition bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table admins ( + machine_id varchar(255) not null, + -- CAC ID of the admin user + common_access_card_id varchar(36) not null primary key, + created_at timestamptz not null default current_timestamp +); + +create table registration_requests ( + id uuid primary key, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + given_name varchar(255) not null, + family_name varchar(255) not null, + address_line_1 varchar(255) not null, + address_line_2 varchar(255), + city varchar(255) not null, + state varchar(16) not null, + postal_code varchar(255) not null, + state_id varchar(255) not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table registrations ( + id uuid primary key, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + registration_request_id uuid not null references registration_requests(id) on update cascade on delete cascade, + election_id uuid not null references elections(id) on update cascade on delete cascade, + precinct_id varchar(255) not null, + ballot_style_id varchar(255) not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table printed_ballots ( + id uuid primary key, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + -- CAC ID of this record's voter + common_access_card_id varchar(36) not null, + common_access_card_certificate bytea not null, + registration_id uuid not null references registrations(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + cast_vote_record_signature bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); + +create table scanned_ballots ( + id uuid primary key, + -- generated on a client machine + client_id uuid not null, + -- ID of the machine this record was originally created on + machine_id varchar(255) not null, + election_id uuid not null references elections(id) on update cascade on delete cascade, + cast_vote_record bytea not null, + created_at timestamptz not null default current_timestamp, + + unique (client_id, machine_id) +); diff --git a/services/rave-server/package.json b/services/rave-server/package.json new file mode 100644 index 0000000000..2a04bc1041 --- /dev/null +++ b/services/rave-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "@votingworks/rave-server", + "version": "1.0.0", + "description": "VotingWorks RAVE Server", + "scripts": { + "postinstall": "sudo apt-get update && sudo apt-get install -y postgresql libssl-dev", + "build": "script/build", + "lint": "script/lint", + "start": "cargo watch -x run", + "test:ci": "script/ci", + "test:dev": "cargo test", + "test": "is-ci test:ci test:dev" + }, + "keywords": [], + "author": "VotingWorks Eng ", + "license": "GPL-3.0", + "devDependencies": { + "is-ci-cli": "2.2.0" + }, + "packageManager": "pnpm@8.1.0" +} diff --git a/services/rave-server/script/build b/services/rave-server/script/build new file mode 100755 index 0000000000..31f6fdd9cb --- /dev/null +++ b/services/rave-server/script/build @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +SQLX_OFFLINE=true cargo build \ No newline at end of file diff --git a/services/rave-server/script/ci b/services/rave-server/script/ci new file mode 100755 index 0000000000..49d36b1895 --- /dev/null +++ b/services/rave-server/script/ci @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +if [ $(id -u) -eq 0 ] && [ "${CI:-}" == true ]; then + service postgresql start + sudo -u postgres createuser --superuser $(whoami) || true + sudo -u postgres createdb $(cat .env | grep DATABASE_URL | cut -d '=' -f 2 - | cut -d ':' -f 2 -) || true +fi + +SQLX_OFFLINE=true cargo test \ No newline at end of file diff --git a/services/rave-server/script/lint b/services/rave-server/script/lint new file mode 100755 index 0000000000..ee15136feb --- /dev/null +++ b/services/rave-server/script/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +cd "${DIR}/.." + +SQLX_OFFLINE=true cargo clippy -- -D warnings \ No newline at end of file diff --git a/services/rave-server/src/app.rs b/services/rave-server/src/app.rs new file mode 100644 index 0000000000..f14b6646fe --- /dev/null +++ b/services/rave-server/src/app.rs @@ -0,0 +1,241 @@ +//! Application definition, including all HTTP route handlers. +//! +//! Route handlers are bundled via [`setup`] into an [`axum::Router`], which can then be run +//! using [`run`] at the configured port (see [`config`][`super::config`]). + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +use axum::{ + extract::{DefaultBodyLimit, State}, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use serde_json::json; +use sqlx::PgPool; +use tower_http::trace::TraceLayer; +use tracing::Level; +use types_rs::rave::{client, RaveServerSyncInput, RaveServerSyncOutput}; + +use crate::{ + config::{Config, MAX_REQUEST_SIZE}, + db, +}; + +type AppState = PgPool; + +/// Prepares the application to be run within an HTTP server. +/// +/// Requires a [`PgPool`] from [`db::setup`]. Run the application with [`run`] +/// with the result of this function. +pub(crate) async fn setup(pool: PgPool) -> color_eyre::Result { + let _entered = tracing::span!(Level::DEBUG, "Setting up application").entered(); + Ok(Router::new() + .route("/api/status", get(get_status)) + .route("/api/admins", post(create_admin)) + .route("/api/sync", post(do_sync)) + .layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE)) + .layer(TraceLayer::new_for_http()) + .with_state(pool)) +} + +/// Create and run an HTTP server using the provided application at the port +/// from [`config`][`super::config`]. +pub(crate) async fn run(app: Router, config: &Config) -> color_eyre::Result<()> { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), config.port); + tracing::info!("Server listening at http://{addr}/"); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await?; + Ok(()) +} + +/// Always responds with a successful status. Used to check whether the server +/// is running. +pub(crate) async fn get_status() -> impl IntoResponse { + StatusCode::OK +} + +/// Synchronizes data between a client and the server. +pub(crate) async fn do_sync( + State(pool): State, + Json(input): Json, +) -> Result, impl IntoResponse> { + let mut txn = match pool.begin().await { + Ok(txn) => txn, + Err(e) => { + tracing::error!("Failed to begin transaction: {}", e); + return Err(Json(json!({ + "success": false, + "error": format!("failed to begin transaction: {}", e) + }))); + } + }; + + let RaveServerSyncInput { + last_synced_registration_request_id, + last_synced_registration_id, + last_synced_election_id, + last_synced_printed_ballot_id, + last_synced_scanned_ballot_id, + registration_requests, + elections, + registrations, + printed_ballots, + scanned_ballots, + } = input; + + for client_request in registration_requests.into_iter() { + let server_request: client::input::RegistrationRequest = client_request; + let result = db::add_registration_request_from_client(&mut txn, &server_request).await; + + if let Err(e) = result { + tracing::error!("Failed to insert registration request: {}", e); + } + } + + for election in elections.into_iter() { + let result = db::add_election(&mut txn, election).await; + + if let Err(e) = result { + tracing::error!("Failed to insert election: {}", e); + } + } + + for registration in registrations.into_iter() { + let result = db::add_registration_from_client(&mut txn, registration).await; + + if let Err(e) = result { + tracing::error!("Failed to insert registration: {}", e); + } + } + + for printed_ballot in printed_ballots.into_iter() { + let result = db::add_printed_ballot_from_client(&mut txn, printed_ballot).await; + + if let Err(e) = result { + tracing::error!("Failed to insert printed ballot: {}", e); + } + } + + for scanned_ballot in scanned_ballots.into_iter() { + let result = db::add_scanned_ballot_from_client(&mut txn, scanned_ballot).await; + + if let Err(e) = result { + tracing::error!("Failed to insert scanned ballot: {}", e); + } + } + + let get_admins_result = db::get_admins(&mut txn).await; + let admins = match get_admins_result { + Err(e) => { + tracing::error!("Failed to get admins: {}", e); + return Err(Json(json!({ "error": e.to_string() }))); + } + Ok(admins) => admins, + }; + + let get_elections_result = db::get_elections(&mut txn, last_synced_election_id).await; + let elections = match get_elections_result { + Err(e) => { + tracing::error!("Failed to get elections: {}", e); + return Err(Json(json!({ "error": e.to_string() }))); + } + Ok(elections) => elections, + }; + + let get_registration_requests_result = + db::get_registration_requests(&mut txn, last_synced_registration_request_id).await; + let registration_requests = match get_registration_requests_result { + Err(e) => { + tracing::error!("Failed to get registration requests: {}", e); + return Err(Json(json!({ "error": e.to_string() }))); + } + Ok(registration_requests) => registration_requests, + }; + + let get_registrations_result = + db::get_registrations(&mut txn, last_synced_registration_id).await; + let registrations = match get_registrations_result { + Err(e) => { + tracing::error!("Failed to get registrations: {}", e); + return Err(Json(json!({ "error": e.to_string() }))); + } + Ok(registrations) => registrations, + }; + + let printed_ballots = + match db::get_printed_ballots(&mut txn, last_synced_printed_ballot_id).await { + Err(e) => { + tracing::error!("Failed to get printed ballots: {}", e); + return Err(Json(json!({ "error": e.to_string() }))); + } + Ok(ballots) => ballots, + }; + + let scanned_ballots = + match db::get_scanned_ballots(&mut txn, last_synced_scanned_ballot_id).await { + Err(e) => { + tracing::error!("Failed to get scanned ballots: {}", e); + return Err(Json(json!({ "error": e.to_string() }))); + } + Ok(ballots) => ballots, + }; + + let output = RaveServerSyncOutput { + admins: admins.into_iter().map(|admin| admin.into()).collect(), + elections: elections + .into_iter() + .map(|election| election.into()) + .collect(), + registration_requests: registration_requests + .into_iter() + .map(|registration_request| registration_request.into()) + .collect(), + registrations: registrations + .into_iter() + .map(|registration| registration.into()) + .collect(), + printed_ballots: printed_ballots + .into_iter() + .map(|ballot| ballot.into()) + .collect(), + scanned_ballots: scanned_ballots + .into_iter() + .map(|ballot| ballot.into()) + .collect(), + }; + + if let Err(err) = txn.commit().await { + tracing::error!("Failed to commit transaction: {}", err); + return Err(Json(json!({ "error": err.to_string() }))); + } + + Ok(Json(output)) +} + +/// Creates an admin user. Admins have elevated privileges when logged into +/// client machines. +pub(crate) async fn create_admin( + State(pool): State, + Json(input): Json, +) -> impl IntoResponse { + let input = input; + let mut connection = match pool.acquire().await { + Ok(connection) => connection, + Err(err) => { + tracing::error!("Failed to acquire connection: {}", err); + return Err(Json(json!({ "error": err.to_string() }))); + } + }; + let result = db::add_admin(&mut connection, input).await; + + result.map_or_else( + |err| { + tracing::error!("Failed to create admin: {}", err); + Err(Json(json!({ "error": err.to_string() }))) + }, + |_| Ok(StatusCode::CREATED), + ) +} diff --git a/services/rave-server/src/config.rs b/services/rave-server/src/config.rs new file mode 100644 index 0000000000..c252e034c8 --- /dev/null +++ b/services/rave-server/src/config.rs @@ -0,0 +1,23 @@ +//! Application configuration. + +use clap::Parser; + +const TEN_MB: usize = 10 * 1024 * 1024; + +pub(crate) const MAX_REQUEST_SIZE: usize = TEN_MB; + +#[derive(Debug, Clone, Parser)] +#[command(author, version, about)] +pub(crate) struct Config { + /// URL of the PostgreSQL database, e.g. `postgres://user:pass@host:port/dbname`. + #[arg(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + /// Port to listen on. + #[arg(long, env = "PORT")] + pub(crate) port: u16, + + /// Log level. + #[arg(long, env = "LOG_LEVEL", default_value = "info")] + pub(crate) log_level: tracing::Level, +} diff --git a/services/rave-server/src/db.rs b/services/rave-server/src/db.rs new file mode 100644 index 0000000000..a8ded5f6be --- /dev/null +++ b/services/rave-server/src/db.rs @@ -0,0 +1,942 @@ +//! Database access for the application. +//! +//! All direct use of [SQLx][`sqlx`] queries should be in this module. When +//! modifying this file, be sure to run `cargo sqlx prepare` in the application +//! root to regenerate the query metadata for offline builds. +//! +//! To enable `cargo sqlx prepare`, install it via `cargo install --locked +//! sqlx-cli`. + +use std::{str::FromStr, time::Duration}; + +use base64_serde::base64_serde_type; +use serde::{Deserialize, Serialize}; +use sqlx::{self, postgres::PgPoolOptions, PgPool}; +use tracing::Level; +use types_rs::{ + election::ElectionHash, + rave::{client, ClientId, ServerId}, +}; + +use crate::config::Config; + +base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD); + +/// Sets up the database pool and runs any pending migrations, returning the +/// pool to be used by the app. +pub(crate) async fn setup(config: &Config) -> color_eyre::Result { + let _entered = tracing::span!(Level::DEBUG, "Setting up database").entered(); + let pool = PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(Duration::from_secs(3)) + .connect(&config.database_url) + .await?; + sqlx::migrate!("db/migrations").run(&pool).await?; + Ok(pool) +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Admin { + pub(crate) machine_id: String, + pub(crate) common_access_card_id: String, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +impl From for client::output::Admin { + fn from(admin: Admin) -> Self { + let Admin { + machine_id, + common_access_card_id, + created_at, + } = admin; + + Self { + machine_id, + common_access_card_id, + created_at, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct Election { + pub(crate) id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) definition: types_rs::election::ElectionDefinition, + pub(crate) election_hash: ElectionHash, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +impl From for client::output::Election { + fn from(election: Election) -> Self { + let Election { + id, + client_id, + machine_id, + definition, + election_hash, + created_at, + } = election; + + Self { + server_id: id, + client_id, + machine_id, + definition, + election_hash, + created_at, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RegistrationRequest { + pub(crate) id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) common_access_card_id: String, + pub(crate) given_name: String, + pub(crate) family_name: String, + pub(crate) address_line_1: String, + pub(crate) address_line_2: Option, + pub(crate) city: String, + pub(crate) state: String, + pub(crate) postal_code: String, + pub(crate) state_id: String, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +impl From for RegistrationRequest { + fn from(request: client::input::RegistrationRequest) -> Self { + let client::input::RegistrationRequest { + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + } = request; + + Self { + id: ServerId::new(), + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at: sqlx::types::time::OffsetDateTime::now_utc(), + } + } +} + +impl From for client::output::RegistrationRequest { + fn from(request: RegistrationRequest) -> Self { + let RegistrationRequest { + id, + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at, + } = request; + + Self { + server_id: id, + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct Registration { + pub(crate) id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) common_access_card_id: String, + pub(crate) registration_request_id: ServerId, + pub(crate) election_id: ServerId, + pub(crate) precinct_id: String, + pub(crate) ballot_style_id: String, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +impl From for client::output::Registration { + fn from(registration: Registration) -> Self { + let Registration { + id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id, + created_at, + } = registration; + + Self { + server_id: id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id, + created_at, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct PrintedBallot { + pub(crate) id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) common_access_card_id: String, + #[serde(with = "Base64Standard")] + pub(crate) common_access_card_certificate: Vec, + pub(crate) registration_id: ServerId, + #[serde(with = "Base64Standard")] + pub(crate) cast_vote_record: Vec, + #[serde(with = "Base64Standard")] + pub(crate) cast_vote_record_signature: Vec, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +impl From for client::output::PrintedBallot { + fn from(ballot: PrintedBallot) -> Self { + let PrintedBallot { + id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature, + created_at, + } = ballot; + + Self { + server_id: id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature, + created_at, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct ScannedBallot { + pub(crate) id: ServerId, + pub(crate) client_id: ClientId, + pub(crate) machine_id: String, + pub(crate) election_id: ServerId, + #[serde(with = "Base64Standard")] + pub(crate) cast_vote_record: Vec, + pub(crate) created_at: sqlx::types::time::OffsetDateTime, +} + +impl From for client::output::ScannedBallot { + fn from(ballot: ScannedBallot) -> Self { + let ScannedBallot { + id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at, + } = ballot; + + Self { + server_id: id, + client_id, + machine_id, + election_id, + cast_vote_record, + created_at, + } + } +} + +pub(crate) async fn add_admin( + executor: &mut sqlx::PgConnection, + admin: client::input::Admin, +) -> color_eyre::Result<()> { + let client::input::Admin { + machine_id, + common_access_card_id, + } = admin; + + sqlx::query!( + r#" + INSERT INTO admins (machine_id, common_access_card_id) + VALUES ($1, $2) + ON CONFLICT (common_access_card_id) DO NOTHING + "#, + machine_id, + common_access_card_id, + ) + .execute(&mut *executor) + .await?; + + Ok(()) +} + +pub(crate) async fn get_admins( + executor: &mut sqlx::PgConnection, +) -> color_eyre::Result> { + sqlx::query_as!( + Admin, + r#" + SELECT + machine_id, + common_access_card_id, + created_at + FROM admins + ORDER BY created_at ASC + "# + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into) +} + +pub(crate) async fn get_elections( + executor: &mut sqlx::PgConnection, + since_election_id: Option, +) -> color_eyre::Result> { + struct ElectionRecord { + id: ServerId, + client_id: ClientId, + machine_id: String, + definition: Vec, + election_hash: String, + created_at: time::OffsetDateTime, + } + + let since_election = match since_election_id { + Some(id) => sqlx::query!( + r#" + SELECT created_at + FROM elections + WHERE id = $1 + "#, + id.as_uuid(), + ) + .fetch_optional(&mut *executor) + .await + .ok(), + None => None, + } + .flatten(); + + let records = match since_election { + Some(election) => { + sqlx::query_as!( + ElectionRecord, + r#" + SELECT + id as "id: _", + client_id as "client_id: _", + machine_id, + definition, + election_hash, + created_at + FROM elections + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + election.created_at + ) + .fetch_all(&mut *executor) + .await? + } + None => { + sqlx::query_as!( + ElectionRecord, + r#" + SELECT + id as "id: _", + client_id as "client_id: _", + machine_id, + definition, + election_hash, + created_at + FROM elections + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await? + } + }; + + records + .into_iter() + .map(|r| { + Ok(Election { + id: r.id, + client_id: r.client_id, + machine_id: r.machine_id, + definition: r.definition.as_slice().try_into()?, + election_hash: ElectionHash::from_str(&r.election_hash)?, + created_at: r.created_at, + }) + }) + .collect::>>() +} + +pub(crate) async fn get_registration_requests( + executor: &mut sqlx::PgConnection, + since_registration_request_id: Option, +) -> color_eyre::Result> { + let since_registration_request = match since_registration_request_id { + Some(id) => { + sqlx::query!( + r#" + SELECT created_at + FROM registration_requests + WHERE id = $1 + "#, + id.as_uuid() + ) + .fetch_optional(&mut *executor) + .await? + } + None => None, + }; + + match since_registration_request { + Some(registration_request) => sqlx::query_as!( + RegistrationRequest, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at + FROM registration_requests + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + registration_request.created_at + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into), + None => sqlx::query_as!( + RegistrationRequest, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id, + created_at + FROM registration_requests + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into), + } +} + +pub(crate) async fn get_registrations( + executor: &mut sqlx::PgConnection, + since_registration_id: Option, +) -> color_eyre::Result> { + let since_registration = match since_registration_id { + Some(registration_id) => sqlx::query!( + r#" + SELECT created_at + FROM registrations + WHERE id = $1 + "#, + registration_id.as_uuid() + ) + .fetch_optional(&mut *executor) + .await + .ok() + .flatten(), + None => None, + }; + + match since_registration { + Some(registration) => sqlx::query_as!( + Registration, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + registration_request_id as "registration_request_id: ServerId", + election_id as "election_id: ServerId", + precinct_id, + ballot_style_id, + created_at + FROM registrations + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + registration.created_at + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into), + None => sqlx::query_as!( + Registration, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + registration_request_id as "registration_request_id: ServerId", + election_id as "election_id: ServerId", + precinct_id, + ballot_style_id, + created_at + FROM registrations + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into), + } +} + +pub(crate) async fn add_registration_request_from_client( + executor: &mut sqlx::PgConnection, + request: &client::input::RegistrationRequest, +) -> color_eyre::Result { + let registration_request_id = ServerId::new(); + + sqlx::query!( + r#" + INSERT INTO registration_requests ( + id, + client_id, + machine_id, + common_access_card_id, + given_name, + family_name, + address_line_1, + address_line_2, + city, + state, + postal_code, + state_id + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + "#, + registration_request_id.as_uuid(), + request.client_id.as_uuid(), + request.machine_id, + request.common_access_card_id, + request.given_name, + request.family_name, + request.address_line_1, + request.address_line_2, + request.city, + request.state, + request.postal_code, + request.state_id + ) + .execute(executor) + .await?; + + Ok(registration_request_id) +} + +pub(crate) async fn add_election( + executor: &mut sqlx::PgConnection, + election: client::input::Election, +) -> Result { + let election_id = ServerId::new(); + let election_definition = election.definition; + + sqlx::query!( + r#" + INSERT INTO elections ( + id, + client_id, + machine_id, + election_hash, + definition + ) + VALUES ($1, $2, $3, $4, $5) + "#, + election_id.as_uuid(), + election.client_id.as_uuid(), + election.machine_id, + election_definition.election_hash.as_str(), + election_definition.election_data + ) + .execute(executor) + .await?; + + Ok(election_id) +} + +pub(crate) async fn add_registration_from_client( + executor: &mut sqlx::PgConnection, + registration: client::input::Registration, +) -> color_eyre::Result { + let registration_id = ServerId::new(); + + sqlx::query!( + r#" + INSERT INTO registrations ( + id, + client_id, + machine_id, + common_access_card_id, + registration_request_id, + election_id, + precinct_id, + ballot_style_id + ) + VALUES ( + $1, $2, $3, $4, + (SELECT id FROM registration_requests WHERE client_id = $5), + (SELECT id FROM elections WHERE client_id = $6), + $7, $8 + ) + "#, + registration_id.as_uuid(), + registration.client_id.as_uuid(), + registration.machine_id, + registration.common_access_card_id, + registration.registration_request_id.as_uuid(), + registration.election_id.as_uuid(), + registration.precinct_id, + registration.ballot_style_id + ) + .execute(&mut *executor) + .await?; + + Ok(registration_id) +} + +pub(crate) async fn add_printed_ballot_from_client( + executor: &mut sqlx::PgConnection, + ballot: client::input::PrintedBallot, +) -> color_eyre::Result { + let ballot_id = ServerId::new(); + + sqlx::query!( + r#" + INSERT INTO printed_ballots ( + id, + client_id, + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id, + cast_vote_record, + cast_vote_record_signature + ) + VALUES ( + $1, $2, $3, $4, $5, + (SELECT id FROM registrations WHERE client_id = $6), + $7, $8 + ) + "#, + ballot_id.as_uuid(), + ballot.client_id.as_uuid(), + ballot.machine_id, + ballot.common_access_card_id, + ballot.common_access_card_certificate, + ballot.registration_id.as_uuid(), + ballot.cast_vote_record, + ballot.cast_vote_record_signature + ) + .execute(&mut *executor) + .await?; + + Ok(ballot_id) +} + +pub(crate) async fn get_printed_ballots( + executor: &mut sqlx::PgConnection, + since_ballot_id: Option, +) -> color_eyre::Result> { + let since_ballot = match since_ballot_id { + Some(id) => sqlx::query!( + r#" + SELECT + created_at + FROM printed_ballots + WHERE id = $1 + "#, + id.as_uuid(), + ) + .fetch_optional(&mut *executor) + .await + .ok() + .flatten(), + None => None, + }; + + struct PrintedBallotRecord { + id: ServerId, + client_id: ClientId, + machine_id: String, + common_access_card_id: String, + common_access_card_certificate: Vec, + registration_id: ServerId, + cast_vote_record: Vec, + cast_vote_record_signature: Vec, + created_at: time::OffsetDateTime, + } + + let records = match since_ballot { + Some(ballot) => { + sqlx::query_as!( + PrintedBallotRecord, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id as "registration_id: ServerId", + cast_vote_record, + cast_vote_record_signature, + created_at + FROM printed_ballots + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + ballot.created_at + ) + .fetch_all(&mut *executor) + .await? + } + None => { + sqlx::query_as!( + PrintedBallotRecord, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + common_access_card_id, + common_access_card_certificate, + registration_id as "registration_id: ServerId", + cast_vote_record, + cast_vote_record_signature, + created_at + FROM printed_ballots + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await? + } + }; + + records + .into_iter() + .map(|r| { + Ok(PrintedBallot { + id: r.id, + client_id: r.client_id, + machine_id: r.machine_id, + common_access_card_id: r.common_access_card_id, + common_access_card_certificate: r.common_access_card_certificate, + registration_id: r.registration_id, + cast_vote_record: r.cast_vote_record, + cast_vote_record_signature: r.cast_vote_record_signature, + created_at: r.created_at, + }) + }) + .collect::>>() +} + +pub(crate) async fn add_scanned_ballot_from_client( + executor: &mut sqlx::PgConnection, + ballot: client::input::ScannedBallot, +) -> color_eyre::Result { + let ballot_id = ServerId::new(); + + sqlx::query!( + r#" + INSERT INTO scanned_ballots ( + id, + client_id, + machine_id, + election_id, + cast_vote_record + ) + VALUES ( + $1, $2, $3, + (SELECT id FROM elections WHERE client_id = $4), + $5 + ) + "#, + ballot_id.as_uuid(), + ballot.client_id.as_uuid(), + ballot.machine_id, + ballot.election_id.as_uuid(), + ballot.cast_vote_record + ) + .execute(&mut *executor) + .await?; + + Ok(ballot_id) +} + +pub(crate) async fn get_scanned_ballots( + executor: &mut sqlx::PgConnection, + since_ballot_id: Option, +) -> color_eyre::Result> { + let since_ballot = match since_ballot_id { + Some(id) => sqlx::query!( + r#" + SELECT + created_at + FROM scanned_ballots + WHERE id = $1 + "#, + id.as_uuid(), + ) + .fetch_optional(&mut *executor) + .await + .ok() + .flatten(), + None => None, + }; + + match since_ballot { + Some(ballot) => sqlx::query_as!( + ScannedBallot, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + election_id as "election_id: ServerId", + cast_vote_record as "cast_vote_record: _", + created_at + FROM scanned_ballots + WHERE created_at > $1 + ORDER BY created_at DESC + "#, + ballot.created_at + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into), + None => sqlx::query_as!( + ScannedBallot, + r#" + SELECT + id as "id: ServerId", + client_id as "client_id: ClientId", + machine_id, + election_id as "election_id: ServerId", + cast_vote_record as "cast_vote_record: _", + created_at + FROM scanned_ballots + ORDER BY created_at DESC + "#, + ) + .fetch_all(&mut *executor) + .await + .map_err(Into::into), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[sqlx::test(migrations = "db/migrations")] + async fn test_admins(pool: sqlx::PgPool) -> color_eyre::Result<()> { + let mut db = pool.acquire().await?; + + add_admin( + &mut db, + client::input::Admin { + machine_id: "machine-id".to_owned(), + common_access_card_id: "1234567890".to_owned(), + }, + ) + .await?; + + let admins = get_admins(&mut db).await?; + + assert_eq!( + admins + .into_iter() + .map(|a| a.common_access_card_id) + .collect::>(), + vec!["1234567890".to_owned()] + ); + + Ok(()) + } +} diff --git a/services/rave-server/src/log.rs b/services/rave-server/src/log.rs new file mode 100644 index 0000000000..1bdea8da2f --- /dev/null +++ b/services/rave-server/src/log.rs @@ -0,0 +1,35 @@ +//! Logging for RAVE Server. +//! +//! RAVE Server uses the `tracing` library for logging. After calling [`setup`], +//! you'll be able to call [`tracing::info!`], [`tracing::span!`], and others to +//! print log messages to `stdout` in a flexible and configurable way. +//! +//! You may use the `RUST_LOG` environment variable to configure logging at +//! runtime (see [`EnvFilter`][`tracing_subscriber::EnvFilter`]). + +use tracing_subscriber::{prelude::*, util::SubscriberInitExt}; + +use crate::config::Config; + +/// Sets up logging for the application. Call this early in the process +/// lifecycle to ensure logs are not silently ignored. +pub(crate) fn setup(config: &Config) -> color_eyre::Result<()> { + color_eyre::install()?; + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive( + format!( + "{}={}", + env!("CARGO_PKG_NAME").replace('-', "_"), + config.log_level + ) + .parse()?, + ) + .from_env_lossy(), + ) + .with(stdout_log) + .init(); + Ok(()) +} diff --git a/services/rave-server/src/main.rs b/services/rave-server/src/main.rs new file mode 100644 index 0000000000..7c18d83d7c --- /dev/null +++ b/services/rave-server/src/main.rs @@ -0,0 +1,64 @@ +//! `rave-server` is the application server for the RAVE voting system. It +//! provides coordination among various types of clients, of which there could +//! be many of each type. There is expected to be a single `rave-server` +//! instance. +//! +//! RAVE Server uses Postgres as its database server and SQLx to connect to it. +//! See the README at the repository root for more information on setup. + +#![warn( + clippy::all, + clippy::todo, + clippy::empty_enum, + clippy::enum_glob_use, + clippy::mem_forget, + clippy::unused_self, + clippy::filter_map_next, + clippy::needless_continue, + clippy::needless_borrow, + clippy::match_wildcard_for_single_variants, + clippy::if_let_mutex, + clippy::mismatched_target_os, + clippy::await_holding_lock, + clippy::match_on_vec_items, + clippy::imprecise_flops, + clippy::suboptimal_flops, + clippy::lossy_float_literal, + clippy::rest_pat_in_fully_bound_structs, + clippy::fn_params_excessive_bools, + clippy::exit, + clippy::inefficient_to_string, + clippy::linkedlist, + clippy::macro_use_imports, + clippy::option_option, + clippy::verbose_file_reads, + clippy::unnested_or_patterns, + clippy::str_to_string, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + missing_debug_implementations, + missing_docs +)] +#![deny(unreachable_pub, private_in_public)] +#![allow(elided_lifetimes_in_paths, clippy::type_complexity)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))] +#![cfg_attr(test, allow(clippy::float_cmp))] +#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))] + +use clap::Parser; + +mod app; +mod config; +mod db; +mod log; + +#[tokio::main] +async fn main() -> color_eyre::Result<()> { + dotenvy::dotenv()?; + let config = config::Config::parse(); + log::setup(&config)?; + let pool = db::setup(&config).await?; + app::run(app::setup(pool).await?, &config).await +} diff --git a/vxsuite.code-workspace b/vxsuite.code-workspace index 9772b0522c..afd12dbff2 100644 --- a/vxsuite.code-workspace +++ b/vxsuite.code-workspace @@ -1,5 +1,13 @@ { "folders": [ + { + "path": "apps/rave-jx-terminal/backend", + "name": "apps/rave-jx-terminal/backend" + }, + { + "path": "apps/rave-jx-terminal/frontend", + "name": "apps/rave-jx-terminal/frontend" + }, { "path": "apps/rave-mark/backend", "name": "apps/rave-mark/backend" @@ -9,8 +17,12 @@ "name": "apps/rave-mark/frontend" }, { - "path": "apps/rave-mark/integration-testing", - "name": "apps/rave-mark/integration-testing" + "path": "apps/rave-scan/backend", + "name": "apps/rave-scan/backend" + }, + { + "path": "apps/rave-scan/frontend", + "name": "apps/rave-scan/frontend" }, { "path": "docs", @@ -32,6 +44,10 @@ "path": "libs/ballot-encoder", "name": "libs/ballot-encoder" }, + { + "path": "libs/ballot-encoder-rs", + "name": "libs/ballot-encoder-rs" + }, { "path": "libs/basics", "name": "libs/basics" @@ -40,6 +56,10 @@ "path": "libs/cdf-schema-builder", "name": "libs/cdf-schema-builder" }, + { + "path": "libs/central-scanner", + "name": "libs/central-scanner" + }, { "path": "libs/db", "name": "libs/db" @@ -104,10 +124,18 @@ "path": "libs/types", "name": "libs/types" }, + { + "path": "libs/types-rs", + "name": "libs/types-rs" + }, { "path": "libs/ui", "name": "libs/ui" }, + { + "path": "libs/ui-rs", + "name": "libs/ui-rs" + }, { "path": "libs/usb-drive", "name": "libs/usb-drive" @@ -123,6 +151,10 @@ { "path": "script", "name": "script" + }, + { + "path": "services/rave-server", + "name": "services/rave-server" } ], "settings": {