forked from Mirrors/keyoxide-web
Compare commits
No commits in common. "dev" and "add-logging" have entirely different histories.
dev
...
add-loggin
73 changed files with 7815 additions and 11382 deletions
7
.gitea/issue_template.md
Normal file
7
.gitea/issue_template.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
<!-- Please search existing issues to avoid duplicates -->
|
||||
|
||||
<!-- If you'd like to propose a new service provider,
|
||||
please do so over at https://community.keyoxide.org -->
|
||||
|
||||
<!-- Feel free to remove these comments -->
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
name: 'Bug'
|
||||
about: 'Report a bug'
|
||||
title: '[BUG] '
|
||||
ref: 'dev'
|
||||
labels:
|
||||
- 'Status/Needs Triage'
|
||||
- Type/Bug
|
||||
---
|
||||
|
||||
### What happened
|
||||
|
||||
|
||||
|
||||
### Proposed solutions
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
name: Claim verification bug
|
||||
about: Report a claim no longer verifying, or not verifying as it should
|
||||
title: ''
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please open this issue in the [doip-js repo](https://codeberg.org/keyoxide/doipjs/issues/new/choose) instead.
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
name: 'Enhancement'
|
||||
about: 'Suggest a new feature or improve an existing one'
|
||||
title: ''
|
||||
ref: 'dev'
|
||||
labels:
|
||||
- 'Status/Needs Triage'
|
||||
- Type/Enhancement
|
||||
---
|
||||
|
||||
### Proposal
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
name: New claim
|
||||
about: Suggest a new service provider or website for identity verification
|
||||
title: ''
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please open this issue in the [doip-js repo](https://codeberg.org/keyoxide/doipjs/issues/new/choose) instead.
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -33,13 +33,4 @@ node_modules
|
|||
ignore
|
||||
dist
|
||||
static
|
||||
logs
|
||||
|
||||
# yarn
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
logs
|
|
@ -1,10 +1,4 @@
|
|||
steps:
|
||||
test:
|
||||
image: node
|
||||
commands:
|
||||
- yarn --pure-lockfile
|
||||
- yarn run test
|
||||
|
||||
pipeline:
|
||||
build-latest-container:
|
||||
when:
|
||||
branch: main
|
||||
|
@ -18,9 +12,6 @@ steps:
|
|||
from_secret: codeberg_password
|
||||
repo: codeberg.org/keyoxide/keyoxide-web
|
||||
tags: latest
|
||||
build_args_from_env:
|
||||
- CI_COMMIT_SHA
|
||||
- CI_COMMIT_BRANCH
|
||||
|
||||
build-tag-container:
|
||||
when:
|
||||
|
@ -35,9 +26,6 @@ steps:
|
|||
from_secret: codeberg_password
|
||||
repo: codeberg.org/keyoxide/keyoxide-web
|
||||
auto_tag: true
|
||||
build_args_from_env:
|
||||
- CI_COMMIT_SHA
|
||||
- CI_COMMIT_BRANCH
|
||||
|
||||
build-dev-container:
|
||||
when:
|
||||
|
@ -51,7 +39,4 @@ steps:
|
|||
password:
|
||||
from_secret: codeberg_password
|
||||
repo: codeberg.org/keyoxide/keyoxide-web
|
||||
tags: dev
|
||||
build_args_from_env:
|
||||
- CI_COMMIT_SHA
|
||||
- CI_COMMIT_BRANCH
|
||||
tags: dev
|
|
@ -1,4 +0,0 @@
|
|||
nodeLinker: node-modules
|
||||
npmScopes:
|
||||
myriation:
|
||||
npmRegistryServer: https://git.myriation.xyz/api/packages/myriation/npm/
|
109
CHANGELOG.md
109
CHANGELOG.md
|
@ -6,115 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [4.2.7] - 2024-02-01
|
||||
### Added
|
||||
- Server version HTTP endpoint
|
||||
- Server version in footer
|
||||
### Changed
|
||||
- Update doipjs to 1.2.9
|
||||
### Notes
|
||||
- The version of keyoxide-web that the server is running can now be requested at the `/.well-known/keyoxide/version` HTTP endpoint. It is also found in the footer of every page.
|
||||
- Version 1.2.9 of doipjs notably adds support for ORCiD claim verification, as well as a couple of performance improvements.
|
||||
|
||||
|
||||
## [4.2.6] - 2024-01-24
|
||||
### Added
|
||||
- Support openpgp4fpr: queries in URL
|
||||
- Proxy routes for openpgp and aspe
|
||||
### Changed
|
||||
- Update doipjs to 1.2.8
|
||||
### Notes
|
||||
- This version notably adds support for OpenPGP and ASPE claim verification.
|
||||
|
||||
## [4.2.5] - 2023-10-09
|
||||
### Changed
|
||||
- Update doipjs to 1.2.7
|
||||
|
||||
## [4.2.4] - 2023-10-09
|
||||
### Changed
|
||||
- Update doipjs to 1.2.6
|
||||
|
||||
## [4.2.3] - 2023-10-05
|
||||
### Added
|
||||
- Themeable profile pages
|
||||
- Apps page
|
||||
### Changed
|
||||
- Update doipjs to 1.2.5
|
||||
- Make Dicebear API domain configurable
|
||||
### Fixed
|
||||
- Catch errors potentially thrown by function
|
||||
- Update JSON schemas
|
||||
- Icon URL generation in profile view
|
||||
### Notes
|
||||
- ASP profiles use the Dicebear API to generate avatars. By default, Keyoxide
|
||||
uses the official api.dicebear.com instance. To use a custom Dicebear instance,
|
||||
set the DICEBEAR_API_HOSTNAME environment variable to its hostname.
|
||||
|
||||
## [4.2.2] - 2023-10-03
|
||||
### Changed
|
||||
- Update doipjs to 1.2.2
|
||||
|
||||
## [4.2.1] - 2023-09-23
|
||||
### Fixed
|
||||
- Tweak the rate limiter parameters
|
||||
|
||||
## [4.2.0] - 2023-09-23
|
||||
### Added
|
||||
- Profile request rate limiter (experimental; opt-in)
|
||||
### Changed
|
||||
- Update doipjs to 1.2.1
|
||||
- Add logging to OpenPGP profile creation
|
||||
- Add debug data to logs
|
||||
### Fixed
|
||||
- Make hash utils aware of ASPE
|
||||
|
||||
## [4.1.1] - 2023-09-21
|
||||
### Changed
|
||||
- Update doipjs to 1.1.0
|
||||
### Fixed
|
||||
- Missing rel=me for ambiguous claims
|
||||
- OpenPGP cache logic
|
||||
|
||||
## [4.1.0] - 2023-09-18
|
||||
### Changed
|
||||
- Redesign
|
||||
- Update doipjs to 1.0.1
|
||||
- Update node to 20
|
||||
- Make https scheme for proxy calls optional
|
||||
- Display site version in footer
|
||||
|
||||
## [4.0.2] - 2023-09-12
|
||||
### Fixed
|
||||
- Handle doip promise rejection
|
||||
- yarn script calls
|
||||
|
||||
## [4.0.1] - 2023-08-28
|
||||
### Fixed
|
||||
- CI docker builds
|
||||
|
||||
## [4.0.0] - 2023-08-28
|
||||
### Added
|
||||
- ASPE support
|
||||
- Rome linting and formatting
|
||||
- API v3
|
||||
### Changed
|
||||
- Updated doipjs to 1.0.0
|
||||
### Fixed
|
||||
- Missing primaryUserIndex
|
||||
### Removed
|
||||
- API v0, v1, v2
|
||||
|
||||
## [3.6.4] - 2023-03-27
|
||||
### Fixed
|
||||
- Missing /graphql proxy API endpoint
|
||||
|
||||
## [3.6.3] - 2023-03-27
|
||||
### Added
|
||||
- Basic logging
|
||||
### Changed
|
||||
- Replaced liberapay & kofi with opencollective
|
||||
- Updated doipjs to 0.18.3
|
||||
|
||||
## [3.6.2] - 2023-03-08
|
||||
### Added
|
||||
- Dark theme
|
||||
|
|
17
Dockerfile
17
Dockerfile
|
@ -1,21 +1,14 @@
|
|||
FROM node:20-alpine as builder
|
||||
FROM node:16-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
RUN corepack enable
|
||||
RUN yarn install --immutable
|
||||
RUN yarn run build:server && yarn run build:static
|
||||
RUN yarn --pure-lockfile
|
||||
RUN yarn run build
|
||||
|
||||
###
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
ARG CI_COMMIT_SHA
|
||||
ARG CI_COMMIT_BRANCH
|
||||
|
||||
ENV COMMIT_SHA=$CI_COMMIT_SHA
|
||||
ENV COMMIT_BRANCH=$CI_COMMIT_BRANCH
|
||||
FROM node:16-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
|
@ -26,4 +19,4 @@ COPY --from=builder /app/static /app/static
|
|||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD node ./dist/
|
||||
CMD node --experimental-fetch ./dist/
|
74
README.md
74
README.md
|
@ -1,15 +1,15 @@
|
|||
# keyoxide-web
|
||||
# Keyoxide
|
||||
|
||||
[![status-badge](https://ci.codeberg.org/api/badges/5919/status.svg)](https://ci.codeberg.org/repos/5919)
|
||||
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=flat)](https://codeberg.org/keyoxide/keyoxide-web/src/branch/main/LICENSE)
|
||||
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
|
||||
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=flat)](https://fosstodon.org/@keyoxide)
|
||||
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
|
||||
[![Drone (self-hosted) with branch](https://img.shields.io/drone/build/keyoxide/keyoxide-web/main?server=https%3A%2F%2Fdrone.keyoxide.org&style=for-the-badge)](https://drone.keyoxide.org/keyoxide/keyoxide-web)
|
||||
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=for-the-badge)](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE)
|
||||
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
|
||||
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=for-the-badge)](https://fosstodon.org/@keyoxide)
|
||||
[![Liberapay receiving](https://img.shields.io/liberapay/receives/keyoxide?style=for-the-badge)](https://liberapay.com/Keyoxide)
|
||||
|
||||
`keyoxide-web` is the web client that powers [keyoxide.org](https://keyoxide.org) and which you can freely host on your own infrastructure.
|
||||
[Keyoxide](https://keyoxide.org) is a modern, secure and decentralized platform to prove your online identity.
|
||||
|
||||
## Hosting keyoxide-web
|
||||
## Self-hosting
|
||||
|
||||
Self-hosting Keyoxide is an important aspect of the project. Users need to trust the Keyoxide instance they're using to reliably verify identities. Making Keyoxide itself decentralized means no one needs to trust a central server. If a friend or family member is hosting a Keyoxide instance, it becomes much easier to trust the instance!
|
||||
|
||||
|
@ -17,50 +17,38 @@ Self-hosting Keyoxide is an important aspect of the project. Users need to trust
|
|||
|
||||
The Docker container allows you to easily self-host the [Keyoxide](https://keyoxide.org) project. To get started, simply run:
|
||||
|
||||
```sh
|
||||
docker run -d -p 3000:3000 codeberg.org/keyoxide/keyoxide-web:latest
|
||||
```
|
||||
`docker run -d -p 3000:3000 keyoxide/keyoxide:stable`
|
||||
|
||||
Keyoxide will now be available by visiting http://localhost:3000.
|
||||
|
||||
More information available in the [documentation](https://docs.keyoxide.org/guides/self-hosting/).
|
||||
|
||||
## Local development
|
||||
|
||||
Install `node` in one of the following ways:
|
||||
|
||||
- [nix](https://nixos.org/guides/install-nix.html) with [direnv](https://direnv.net/)
|
||||
- using [fnm](https://github.com/Schniz/fnm)
|
||||
- using [nvm](https://github.com/nvm-sh/nvm)
|
||||
- directly from their [website](https://nodejs.org/)
|
||||
|
||||
Install dependencies with `npm install` or `yarn`.
|
||||
|
||||
Run the server with `npm dev` or `yarn dev`. The Keyoxide web client will now be available at [https://localhost:3000](https://localhost:3000).
|
||||
More information available in the [documentation](docs.keyoxide.org/self-hosting).
|
||||
|
||||
## Contributing
|
||||
|
||||
Anyone can contribute!
|
||||
Anyone can contribute if they'd like! No need to be a programmer or technically-oriented for that matter.
|
||||
|
||||
Developers are invited to:
|
||||
Contributing to Keyoxide can happen in many forms:
|
||||
|
||||
- fork the repository and play around
|
||||
- submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/keyoxide-web/issues)
|
||||
|
||||
If you are new to contributing to open source software, we'd love to help you! To get started, here's a [list of "good first issues"](https://codeberg.org/keyoxide/keyoxide-web/issues?q=&type=all&state=open&labels=183598) that you could look into.
|
||||
|
||||
Everyone is invited to:
|
||||
|
||||
- find and [report bugs](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
|
||||
- suggesting [new features](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
|
||||
- [help with translations](https://translate.codeberg.org/projects/keyoxide/)
|
||||
- [improve documentation](https://codeberg.org/keyoxide/keyoxide-docs)
|
||||
- start using open source software and promote it
|
||||
- Finding and reporting bugs
|
||||
- Suggesting new features
|
||||
- Improving documentation
|
||||
- Writing code to fix bugs and features
|
||||
- Promoting decentralized identity and web3.0
|
||||
|
||||
Please note that this project has a [Code of Conduct](https://codeberg.org/keyoxide/web/src/branch/main/CODE_OF_CONDUCT.md) that all contributors agree to abide when participating.
|
||||
|
||||
## About the Keyoxide project
|
||||
### Local development
|
||||
|
||||
The Keyoxide project strives for a healthier internet for all and has made its efforts fully [open source](https://codeberg.org/keyoxide). Our [community](https://docs.keyoxide.org/community/) is open and welcoming, feel free to say hi!
|
||||
To run Keyoxide locally on your machine for development:
|
||||
|
||||
Funding for the project comes from the [NLnet foundation](https://nlnet.nl/), [NGI0](https://www.ngi.eu/) and the people supporting our [OpenCollective](https://opencollective.com/keyoxide). The project is grateful for all your support.
|
||||
- install either
|
||||
- NodeJS
|
||||
- directly from their [website](https://nodejs.org/en/), or
|
||||
- using [nvm](https://github.com/nvm-sh/nvm): `nvm install --lts; nvm use --lts`
|
||||
- [yarn](https://yarnpkg.com/)
|
||||
- [nix](https://nixos.org/guides/install-nix.html) with
|
||||
[direnv](https://direnv.net/) will install yarn and other dependencies.
|
||||
- install dependencies with `npm install` or `yarn`
|
||||
- run the server with `npm dev` or `yarn dev`
|
||||
|
||||
Keyoxide will now be available at [https://localhost:3000](https://localhost:3000)
|
||||
|
|
15
biome.json
15
biome.json
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.2.2/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": false
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
|
@ -8,21 +8,21 @@ There are no accounts on Keyoxide. Never does Keyoxide need to know any of your
|
|||
|
||||
## Profile pages
|
||||
|
||||
To generate a profile page, Keyoxide will look at the URL and fetch the associated OpenPGP key or ASP profile. The fetched document is parsed on the server, the claims are rendered unverified then sent to the browser. The browser will then attempt to verify the claims by fetching the required proofs, usually JSON documents.
|
||||
To generate a profile page, Keyoxide will look at the URL and fetch the associated OpenPGP key. The OpenPGP key is parsed on the server and the claims are rendered unverified then sent to the browser. The browser will then attempt to verify the claims by fetching the required proofs, usually JSON documents.
|
||||
|
||||
If a browser cannot fetch the proof (for example, due to CORS restraints), it will ask a proxy server to fetch the proof instead if a proxy is configured by the instance administrator.
|
||||
|
||||
Profile pages and claim verifications are not cached on the server. OpenPGP keys may be cached for a minute to alleviate strain on keyservers.
|
||||
OpenPGP keys, profile pages and claim verifications are not cached on the server.
|
||||
|
||||
## Donations
|
||||
|
||||
Donations are handled by OpenCollective. Keyoxide does not store personal or payment-related information.
|
||||
Donations are handled by Stripe. Keyoxide does not store personal or payment-related information.
|
||||
|
||||
How OpenCollective processes the data is covered by the [OpenCollective Privacy Policy](https://opencollective.com/privacypolicy).
|
||||
How Stripe processes the data is covered by the [Stripe privacy policy](https://stripe.com/privacy).
|
||||
|
||||
## Keyoxide instances
|
||||
|
||||
Each Keyoxide instance is administered by different people using different infrastructure and different configurations. This document cannot account for the way each particular instance handles/stores/processes the HTTP requests.
|
||||
Each Keyoxide instance is administered by different people using different technologies and different configurations. This document cannot account for the way each particular instance handles/stores/processes the HTTP requests.
|
||||
|
||||
## Keyoxide.org instance
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@ services:
|
|||
- 3000:3000
|
||||
environment:
|
||||
- DOMAIN=
|
||||
# - KX_HIGHLIGHTS_1_NAME=
|
||||
# - KX_HIGHLIGHTS_1_DESCRIPTION=
|
||||
# - KX_HIGHLIGHTS_1_FINGERPRINT=
|
||||
## The hostname to reach the doip_proxy container below
|
||||
# - PROXY_HOSTNAME=
|
||||
## The onion URL to advertise in the HTTP response headers
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
[Unit]
|
||||
Description=Keyoxide (Online identity verification)
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=keyoxide
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/keyoxide-web/
|
||||
ExecStart=/usr/bin/node /opt/keyoxide-web/dist/index.js
|
||||
Restart=always
|
||||
RestartSec=2s
|
||||
Environment=PORT=5000 DOMAIN=domain.example PROXY_HOSTNAME=domain.example
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"execArgs": [
|
||||
"--experimental-fetch"
|
||||
],
|
||||
"env": {
|
||||
"NODE_ENV": "development",
|
||||
"LOG_LEVEL": "debug"
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"ext": "js,json,css,pug,md"
|
||||
}
|
44
package.json
44
package.json
|
@ -1,69 +1,59 @@
|
|||
{
|
||||
"name": "keyoxide-web",
|
||||
"version": "4.2.7",
|
||||
"version": "3.6.2",
|
||||
"description": "Verifying online identity with cryptography",
|
||||
"main": "./src/index.js",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@3.6.1",
|
||||
"dependencies": {
|
||||
"ajv": "^8.6.3",
|
||||
"bent": "^7.3.12",
|
||||
"body-parser": "^1.19.0",
|
||||
"colorjs.io": "^0.4.5",
|
||||
"doipjs": "npm:@myriation/doipjs@1.2.9+myriation.1",
|
||||
"dialog-polyfill": "^0.5.6",
|
||||
"doipjs": "^0.18.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-http-context2": "^1.0.0",
|
||||
"express-rate-limit": "^7.0.1",
|
||||
"express-validator": "^6.13.0",
|
||||
"fork-awesome": "^1.2.0",
|
||||
"got": "^11.8.2",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"jstransformer-markdown-it": "^3.0.0",
|
||||
"keyv": "^4.5.0",
|
||||
"libravatar": "^3.0.0",
|
||||
"nanoid": "^5.0.1",
|
||||
"openpgp": "^5.5.0",
|
||||
"pug": "^3.0.2",
|
||||
"pug": "^3.0.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"string-replace-middleware": "^1.0.2",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.2.2",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"chai": "^4.3.6",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"copy-webpack-plugin": "^10.2.4",
|
||||
"css-loader": "^6.6.0",
|
||||
"esmock": "^2.5.0",
|
||||
"license-check-and-add": "^4.0.5",
|
||||
"mini-css-extract-plugin": "^2.5.3",
|
||||
"mocha": "^10.1.0",
|
||||
"nodemon": "^3.0.3",
|
||||
"sass": "^1.67.0",
|
||||
"sass-loader": "^13.3.2",
|
||||
"nodemon": "^2.0.20",
|
||||
"standard": "^17.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-bundle-analyzer": "^4.7.0",
|
||||
"webpack-cli": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./",
|
||||
"start": "node --experimental-fetch ./",
|
||||
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev",
|
||||
"test": "yarn run lint && mocha",
|
||||
"watch": "nodemon --config nodemon.json ./",
|
||||
"build": "yarn run build:server && yarn run build:static",
|
||||
"test": "yarn run standard:check && mocha",
|
||||
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
|
||||
"build": "yarn run build:server & yarn run build:static",
|
||||
"build:server": "ncc build ./src/index.js -o dist",
|
||||
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production",
|
||||
"build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
|
||||
"lint": "yarn run standard:check && yarn run biome:check",
|
||||
"biome:check": "biome check ./src && biome lint ./src",
|
||||
"biome:fix": "biome check --apply ./src && biome lint --apply ./src",
|
||||
"standard:check": "standard ./src",
|
||||
"standard:fix": "standard --fix ./src",
|
||||
"license:check": "license-check-and-add check",
|
||||
"license:add": "license-check-and-add add",
|
||||
"license:remove": "license-check-and-add remove"
|
||||
"standard:check": "./node_modules/.bin/standard ./src",
|
||||
"standard:fix": "./node_modules/.bin/standard --fix ./src",
|
||||
"license:check": "./node_modules/.bin/license-check-and-add check",
|
||||
"license:add": "./node_modules/.bin/license-check-and-add add",
|
||||
"license:remove": "./node_modules/.bin/license-check-and-add remove"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
399
src/api/v0/index.js
Normal file
399
src/api/v0/index.js
Normal file
|
@ -0,0 +1,399 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { check, validationResult } from 'express-validator'
|
||||
import Ajv from 'ajv'
|
||||
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv({ coerceTypes: true })
|
||||
|
||||
const apiProfileSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fingerprint: {
|
||||
type: 'string'
|
||||
},
|
||||
openpgp4fpr: {
|
||||
type: 'string'
|
||||
},
|
||||
users: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
isPrimary: { type: 'boolean' },
|
||||
isRevoked: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
claims: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
claimVersion: { type: 'integer' },
|
||||
uri: { type: 'string' },
|
||||
fingerprint: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
matches: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
serviceProvider: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string' },
|
||||
name: { type: 'string' }
|
||||
}
|
||||
},
|
||||
match: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
regularExpression: { type: 'object' },
|
||||
isAmbiguous: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
display: { type: 'string' },
|
||||
uri: { type: 'string' },
|
||||
qr: { type: 'string' }
|
||||
}
|
||||
},
|
||||
proof: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string' },
|
||||
request: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fetcher: { type: 'string' },
|
||||
access: { type: 'string' },
|
||||
format: { type: 'string' },
|
||||
data: { type: 'object' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
format: { type: 'string' },
|
||||
relation: { type: 'string' },
|
||||
path: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
verification: {
|
||||
type: 'object'
|
||||
},
|
||||
summary: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
profileName: { type: 'string' },
|
||||
profileURL: { type: 'string' },
|
||||
serviceProviderName: { type: 'string' },
|
||||
isVerificationDone: { type: 'boolean' },
|
||||
isVerified: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
primaryUserIndex: {
|
||||
type: 'integer'
|
||||
},
|
||||
key: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object' },
|
||||
fetchMethod: { type: 'string' },
|
||||
uri: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
keyoxide: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' }
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarURL: { type: 'string' }
|
||||
}
|
||||
},
|
||||
errors: {
|
||||
type: 'array'
|
||||
}
|
||||
},
|
||||
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
||||
|
||||
const doVerification = async (data) => {
|
||||
const promises = []
|
||||
const results = []
|
||||
const verificationOptions = {
|
||||
proxy: {
|
||||
hostname: process.env.PROXY_HOSTNAME,
|
||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||
const user = data.keyData.users[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
await claim.verify(verificationOptions)
|
||||
results.push([iUser, iClaim, claim])
|
||||
resolve()
|
||||
})()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const sanitize = (data) => {
|
||||
const dataClone = JSON.parse(JSON.stringify(data))
|
||||
|
||||
if (dataClone.keyData.users) {
|
||||
for (let iUser = 0; iUser < dataClone.keyData.users.length; iUser++) {
|
||||
const user = dataClone.keyData.users[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
// TODO Fix upstream
|
||||
for (let iMatch = 0; iMatch < claim.matches.length; iMatch++) {
|
||||
const match = claim.matches[iMatch]
|
||||
if (Array.isArray(match.claim)) {
|
||||
match.claim = match.claim[0]
|
||||
}
|
||||
}
|
||||
// TODO Fix upstream
|
||||
if (!claim.verification) {
|
||||
claim.verification = {}
|
||||
}
|
||||
// TODO Fix upstream
|
||||
claim.matches.forEach(match => {
|
||||
match.proof.request.access = ['generic', 'nocors', 'granted', 'server'][match.proof.request.access]
|
||||
match.claim.format = ['uri', 'fingerprint', 'message'][match.claim.format]
|
||||
match.claim.relation = ['contains', 'equals', 'oneof'][match.claim.relation]
|
||||
})
|
||||
|
||||
data.keyData.users[iUser].claims[iClaim] = claim
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const valid = apiProfileValidate(data)
|
||||
if (!valid) {
|
||||
throw new Error('Profile data sanitization error')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const addSummaryToClaims = (data) => {
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
// To be removed when data is added by DOIP library
|
||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
||||
const user = data.keyData.users[userIndex]
|
||||
|
||||
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
||||
const claim = user.claims[claimIndex]
|
||||
|
||||
const isVerificationDone = claim.status === 'verified'
|
||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
||||
const isAmbiguous = isVerified
|
||||
? false
|
||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
||||
|
||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
||||
isVerificationDone,
|
||||
isVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
router.get('/profile/fetch',
|
||||
check('query').exists(),
|
||||
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
|
||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
||||
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate profile
|
||||
let data
|
||||
switch (req.query.protocol) {
|
||||
case 'wkd':
|
||||
data = await generateWKDProfile(req.query.query)
|
||||
break
|
||||
case 'hkp':
|
||||
data = await generateHKPProfile(req.query.query)
|
||||
break
|
||||
default:
|
||||
data = await generateAutoProfile(req.query.query)
|
||||
break
|
||||
}
|
||||
|
||||
if (data.errors.length > 0) {
|
||||
delete data.key
|
||||
res.status(500).send(data)
|
||||
}
|
||||
|
||||
// Return public key
|
||||
if (req.query.returnPublicKey) {
|
||||
data.keyData.key.data = data.key.publicKey
|
||||
}
|
||||
delete data.key
|
||||
|
||||
// Do verification
|
||||
if (req.query.doVerification) {
|
||||
data = await doVerification(data)
|
||||
}
|
||||
|
||||
try {
|
||||
// Sanitize JSON
|
||||
data = sanitize(data)
|
||||
} catch (error) {
|
||||
data.keyData = {}
|
||||
data.extra = {}
|
||||
data.errors = [error.message]
|
||||
}
|
||||
|
||||
// Add missing data
|
||||
data = addSummaryToClaims(data)
|
||||
|
||||
let statusCode = 200
|
||||
if (data.errors.length > 0) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
router.get('/profile/verify',
|
||||
check('data').exists().isJSON(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// Do verification
|
||||
let data = await doVerification(req.query.data)
|
||||
|
||||
try {
|
||||
// Sanitize JSON
|
||||
data = sanitize(data)
|
||||
} catch (error) {
|
||||
data.keyData = {}
|
||||
data.extra = {}
|
||||
data.errors = [error.message]
|
||||
}
|
||||
|
||||
// Add missing data
|
||||
data = addSummaryToClaims(data)
|
||||
|
||||
let statusCode = 200
|
||||
if (data.errors.length > 0) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
23
src/api/v1/index.js
Normal file
23
src/api/v1/index.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
Copyright 2022 Yarmo Mackenbach
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import express from 'express'
|
||||
const router = express.Router()
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v2 API endpoint')
|
||||
})
|
||||
|
||||
export default router
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
Copyright (C) 2022 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
370
src/api/v2/keyoxide_profile.js
Normal file
370
src/api/v2/keyoxide_profile.js
Normal file
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
Copyright (C) 2022 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { check, validationResult } from 'express-validator'
|
||||
import Ajv from 'ajv'
|
||||
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv({ coerceTypes: true })
|
||||
|
||||
const apiProfileSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fingerprint: {
|
||||
type: 'string'
|
||||
},
|
||||
openpgp4fpr: {
|
||||
type: 'string'
|
||||
},
|
||||
users: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
comment: { type: 'string' },
|
||||
isPrimary: { type: 'boolean' },
|
||||
isRevoked: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
claims: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
claimVersion: { type: 'integer' },
|
||||
uri: { type: 'string' },
|
||||
fingerprint: { type: 'string' },
|
||||
status: { type: 'string' },
|
||||
matches: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
serviceProvider: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { type: 'string' },
|
||||
name: { type: 'string' }
|
||||
}
|
||||
},
|
||||
match: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
regularExpression: { type: 'object' },
|
||||
isAmbiguous: { type: 'boolean' }
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
display: { type: 'string' },
|
||||
uri: { type: 'string' },
|
||||
qr: { type: 'string' }
|
||||
}
|
||||
},
|
||||
proof: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: { type: 'string' },
|
||||
request: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fetcher: { type: 'string' },
|
||||
access: { type: 'string' },
|
||||
format: { type: 'string' },
|
||||
data: { type: 'object' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
format: { type: 'string' },
|
||||
relation: { type: 'string' },
|
||||
path: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
verification: {
|
||||
type: 'object'
|
||||
},
|
||||
summary: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
profileName: { type: 'string' },
|
||||
profileURL: { type: 'string' },
|
||||
serviceProviderName: { type: 'string' },
|
||||
isVerificationDone: { type: 'boolean' },
|
||||
isVerified: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
primaryUserIndex: {
|
||||
type: 'integer'
|
||||
},
|
||||
key: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object' },
|
||||
fetchMethod: { type: 'string' },
|
||||
uri: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
keyoxide: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string' }
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarURL: { type: 'string' }
|
||||
}
|
||||
},
|
||||
errors: {
|
||||
type: 'array'
|
||||
}
|
||||
},
|
||||
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
||||
|
||||
const doVerification = async (data) => {
|
||||
const promises = []
|
||||
const results = []
|
||||
const verificationOptions = {
|
||||
proxy: {
|
||||
hostname: process.env.PROXY_HOSTNAME,
|
||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||
const user = data.keyData.users[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
await claim.verify(verificationOptions)
|
||||
results.push([iUser, iClaim, claim])
|
||||
resolve()
|
||||
})()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const sanitize = (data) => {
|
||||
const valid = apiProfileValidate(data)
|
||||
if (!valid) {
|
||||
throw new Error('Profile data sanitization error')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const addSummaryToClaims = (data) => {
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
// To be removed when data is added by DOIP library
|
||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
||||
const user = data.keyData.users[userIndex]
|
||||
|
||||
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
||||
const claim = user.claims[claimIndex]
|
||||
|
||||
const isVerificationDone = claim.status === 'verified'
|
||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
||||
const isAmbiguous = isVerified
|
||||
? false
|
||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
||||
|
||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
||||
isVerificationDone,
|
||||
isVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
router.get('/fetch',
|
||||
check('query').exists(),
|
||||
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
|
||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
||||
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate profile
|
||||
let data
|
||||
switch (req.query.protocol) {
|
||||
case 'wkd':
|
||||
data = await generateWKDProfile(req.query.query)
|
||||
break
|
||||
case 'hkp':
|
||||
data = await generateHKPProfile(req.query.query)
|
||||
break
|
||||
default:
|
||||
data = await generateAutoProfile(req.query.query)
|
||||
break
|
||||
}
|
||||
|
||||
if (data.errors.length > 0) {
|
||||
delete data.key
|
||||
res.status(500).send(data)
|
||||
}
|
||||
|
||||
// Return public key
|
||||
if (req.query.returnPublicKey) {
|
||||
data.keyData.key.data = data.key.publicKey
|
||||
}
|
||||
delete data.key
|
||||
|
||||
// Do verification
|
||||
if (req.query.doVerification) {
|
||||
data = await doVerification(data)
|
||||
}
|
||||
|
||||
try {
|
||||
// Sanitize JSON
|
||||
data = sanitize(data)
|
||||
} catch (error) {
|
||||
data.keyData = {}
|
||||
data.extra = {}
|
||||
data.errors = [error.message]
|
||||
}
|
||||
|
||||
// Add missing data
|
||||
data = addSummaryToClaims(data)
|
||||
|
||||
let statusCode = 200
|
||||
if (data.errors.length > 0) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
router.get('/verify',
|
||||
check('data').exists().isJSON(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// Do verification
|
||||
let data = await doVerification(req.query.data)
|
||||
|
||||
try {
|
||||
// Sanitize JSON
|
||||
data = sanitize(data)
|
||||
} catch (error) {
|
||||
data.keyData = {}
|
||||
data.extra = {}
|
||||
data.errors = [error.message]
|
||||
}
|
||||
|
||||
// Add missing data
|
||||
data = addSummaryToClaims(data)
|
||||
|
||||
let statusCode = 200
|
||||
if (data.errors.length > 0) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
|
@ -42,12 +42,7 @@ const opts = {
|
|||
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
|
||||
},
|
||||
irc: {
|
||||
nick: process.env.IRC_NICK || null,
|
||||
sasl: Object.keys(process.env).filter(k => k.startsWith('IRC_SASL_USERNAME_')).map(k => ({
|
||||
username: process.env[k],
|
||||
password: process.env[`IRC_SASL_PASSWORD_${k.substring('IRC_SASL_USERNAME_'.length)}`],
|
||||
domainRegex: process.env[`IRC_SASL_DOMAIN_REGEX_${k.substring('IRC_SASL_USERNAME_'.length)}`]
|
||||
}))
|
||||
nick: process.env.IRC_NICK || null
|
||||
},
|
||||
matrix: {
|
||||
instance: process.env.MATRIX_INSTANCE || null,
|
||||
|
@ -56,6 +51,9 @@ const opts = {
|
|||
telegram: {
|
||||
token: process.env.TELEGRAM_TOKEN || null
|
||||
},
|
||||
twitter: {
|
||||
bearerToken: process.env.TWITTER_BEARER_TOKEN || null
|
||||
},
|
||||
xmpp: {
|
||||
service: process.env.XMPP_SERVICE || null,
|
||||
username: process.env.XMPP_USERNAME || null,
|
||||
|
@ -97,40 +95,6 @@ router.get(
|
|||
}
|
||||
)
|
||||
|
||||
// ASPE route
|
||||
router.get('/aspe', query('aspeUri').isString(), (req, res) => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() })
|
||||
}
|
||||
|
||||
fetcher.aspe
|
||||
.fn(req.query, opts)
|
||||
.then((data) => {
|
||||
return res.status(200).send(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||
})
|
||||
})
|
||||
|
||||
// OpenPGP route
|
||||
router.get('/openpgp', query('url').isURL(), query('protocol').isString(), (req, res) => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() })
|
||||
}
|
||||
|
||||
fetcher.openpgp
|
||||
.fn(req.query, opts)
|
||||
.then((data) => {
|
||||
return res.status(200).send(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||
})
|
||||
})
|
||||
|
||||
// DNS route
|
||||
router.get('/dns', query('domain').isFQDN(), (req, res) => {
|
||||
const errors = validationResult(req)
|
||||
|
@ -154,7 +118,9 @@ router.get(
|
|||
query('id').isEmail(),
|
||||
async (req, res) => {
|
||||
if (
|
||||
!((opts.claims.xmpp.service && opts.claims.xmpp.username) && opts.claims.xmpp.password)
|
||||
!opts.claims.xmpp.service ||
|
||||
!opts.claims.xmpp.username ||
|
||||
!opts.claims.xmpp.password
|
||||
) {
|
||||
return res.status(501).json({ errors: 'XMPP not enabled on server' })
|
||||
}
|
||||
|
@ -174,13 +140,33 @@ router.get(
|
|||
}
|
||||
)
|
||||
|
||||
// Twitter route
|
||||
router.get('/twitter', query('tweetId').isInt(), async (req, res) => {
|
||||
if (!opts.claims.twitter.bearerToken) {
|
||||
return res.status(501).json({ errors: 'Twitter not enabled on server' })
|
||||
}
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() })
|
||||
}
|
||||
|
||||
fetcher.twitter
|
||||
.fn(req.query, opts)
|
||||
.then((data) => {
|
||||
return res.status(200).send(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||
})
|
||||
})
|
||||
|
||||
// Matrix route
|
||||
router.get(
|
||||
'/matrix',
|
||||
query('roomId').isString(),
|
||||
query('eventId').isString(),
|
||||
async (req, res) => {
|
||||
if (!(opts.claims.matrix.instance && opts.claims.matrix.accessToken)) {
|
||||
if (!opts.claims.matrix.instance || !opts.claims.matrix.accessToken) {
|
||||
return res.status(501).json({ errors: 'Matrix not enabled on server' })
|
||||
}
|
||||
const errors = validationResult(req)
|
||||
|
@ -291,26 +277,4 @@ router.get(
|
|||
}
|
||||
)
|
||||
|
||||
// GraphQL route
|
||||
router.get(
|
||||
'/graphql',
|
||||
query('url').isURL(),
|
||||
query('query').isString(),
|
||||
async (req, res) => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() })
|
||||
}
|
||||
|
||||
fetcher.graphql
|
||||
.fn(req.query, opts)
|
||||
.then((data) => {
|
||||
return res.status(200).send(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
|
@ -1,197 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { check, validationResult } from 'express-validator'
|
||||
import Ajv from 'ajv/dist/2020.js'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { Claim } from 'doipjs'
|
||||
import { generateAspeProfile, generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||
import { claimSchema, personaSchema, profileSchema, serviceProviderSchema } from '../../schemas.js'
|
||||
dotenv.config()
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv({
|
||||
schemas: [profileSchema, personaSchema, claimSchema, serviceProviderSchema]
|
||||
})
|
||||
|
||||
const apiProfileValidate = ajv.compile(profileSchema)
|
||||
|
||||
const doVerification = async (profile) => {
|
||||
const promises = []
|
||||
const results = []
|
||||
const verificationOptions = {
|
||||
proxy: {
|
||||
hostname: process.env.PROXY_HOSTNAME,
|
||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if no users in key
|
||||
if (!profile.personas) {
|
||||
return profile
|
||||
}
|
||||
|
||||
for (let iUser = 0; iUser < profile.personas.length; iUser++) {
|
||||
const user = profile.personas[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
await claim.verify(verificationOptions)
|
||||
results.push([iUser, iClaim, claim])
|
||||
resolve()
|
||||
})()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
profile.personas[result[0]].claims[result[1]] = result[2]
|
||||
})
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
const validate = (profile) => {
|
||||
const valid = apiProfileValidate(profile)
|
||||
if (!valid) {
|
||||
throw new Error(`Profile data validation error: ${apiProfileValidate.errors.map(x => x.message).join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/fetch',
|
||||
check('query').exists(),
|
||||
check('protocol').optional().toLowerCase().isIn(['aspe', 'hkp', 'wkd']),
|
||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate profile
|
||||
let data
|
||||
switch (req.query.protocol) {
|
||||
case 'aspe':
|
||||
data = await generateAspeProfile(req.query.query)
|
||||
break
|
||||
case 'wkd':
|
||||
data = await generateWKDProfile(req.query.query)
|
||||
break
|
||||
case 'hkp':
|
||||
data = await generateHKPProfile(req.query.query)
|
||||
break
|
||||
default:
|
||||
data = await generateAutoProfile(req.query.query)
|
||||
break
|
||||
}
|
||||
|
||||
if ('errors' in data && data.errors.length > 0) {
|
||||
res.status(500).send(data)
|
||||
}
|
||||
|
||||
// Do verification
|
||||
if (req.query.doVerification) {
|
||||
data = await doVerification(data)
|
||||
}
|
||||
|
||||
try {
|
||||
data = data.toJSON()
|
||||
} catch (error) {
|
||||
data = {
|
||||
errors: [error.message]
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate JSON
|
||||
validate(data)
|
||||
} catch (error) {
|
||||
data = {
|
||||
errors: [error.message]
|
||||
}
|
||||
}
|
||||
|
||||
let statusCode = 200
|
||||
if ('errors' in data && data.errors.length > 0) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
router.get('/verify',
|
||||
check('data').exists().isJSON(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
const profile = Claim.fromJson(req.query.data)
|
||||
|
||||
// Do verification
|
||||
let data = await doVerification(profile)
|
||||
|
||||
try {
|
||||
data = data.toJSON()
|
||||
} catch (error) {
|
||||
data = {
|
||||
errors: [error.message]
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate JSON
|
||||
validate(data)
|
||||
} catch (error) {
|
||||
data = {
|
||||
errors: [error.message]
|
||||
}
|
||||
}
|
||||
|
||||
let statusCode = 200
|
||||
if ('errors' in data) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
32
src/index.js
32
src/index.js
|
@ -28,10 +28,7 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import * as httpContext from 'express-http-context2'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { readFileSync } from 'fs'
|
||||
import { execSync } from 'child_process'
|
||||
import { stringReplace } from 'string-replace-middleware'
|
||||
import * as pug from 'pug'
|
||||
import * as dotenv from 'dotenv'
|
||||
|
@ -44,20 +41,6 @@ import staticRoute from './routes/static.js'
|
|||
import utilRoute from './routes/util.js'
|
||||
dotenv.config()
|
||||
|
||||
// Get information about the last git commit
|
||||
let gitBranch = process.env.CI_COMMIT_BRANCH ?? process.env.COMMIT_BRANCH
|
||||
if (!gitBranch) {
|
||||
try {
|
||||
gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
|
||||
} catch (_) {}
|
||||
}
|
||||
let gitHash = process.env.CI_COMMIT_SHA ?? process.env.COMMIT_SHA
|
||||
if (!gitHash) {
|
||||
try {
|
||||
gitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const app = express()
|
||||
const packageData = JSON.parse(readFileSync('./package.json'))
|
||||
|
||||
|
@ -65,24 +48,12 @@ app.set('env', process.env.NODE_ENV || 'production')
|
|||
app.engine('pug', pug.__express).set('view engine', 'pug')
|
||||
app.set('port', process.env.PORT || 3000)
|
||||
app.set('domain', process.env.DOMAIN)
|
||||
app.set('scheme', process.env.SCHEME || 'https')
|
||||
app.set('keyoxide_name', 'keyoxide-web')
|
||||
app.set('keyoxide_version', packageData.version)
|
||||
app.set('git_branch', gitBranch)
|
||||
app.set('git_hash', gitHash)
|
||||
app.set('onion_url', process.env.ONION_URL)
|
||||
|
||||
// Middlewares
|
||||
app.use(httpContext.middleware)
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Permissions-Policy', 'interest-cohort=()')
|
||||
httpContext.set('requestId', nanoid())
|
||||
httpContext.set('requestPath', req.path)
|
||||
httpContext.set('requestIp', req.ip)
|
||||
|
||||
logger.info('Handle a request',
|
||||
{ component: 'http_server', action: 'request' })
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
|
@ -94,8 +65,7 @@ if (app.get('onion_url')) {
|
|||
}
|
||||
|
||||
app.use(stringReplace({
|
||||
PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || process.env.DOMAIN || 'null',
|
||||
PLACEHOLDER__PROXY_SCHEME: process.env.PROXY_SCHEME || process.env.SCHEME || 'https'
|
||||
PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || process.env.DOMAIN || 'null'
|
||||
}, {
|
||||
contentTypeFilterRegexp: /application\/javascript/
|
||||
}))
|
||||
|
|
11
src/log.js
11
src/log.js
|
@ -28,7 +28,6 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { createLogger, format, transports } from 'winston'
|
||||
import * as httpContext from 'express-http-context2'
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
|
@ -38,23 +37,13 @@ const anonymize = format((info, opts) => {
|
|||
info.keyserver_domain = undefined
|
||||
info.username = undefined
|
||||
info.fingerprint = undefined
|
||||
info.request_path = undefined
|
||||
info.request_ip = undefined
|
||||
}
|
||||
return info
|
||||
})
|
||||
|
||||
const addRequestData = format((info, opts) => {
|
||||
if (httpContext.get('requestId')) info.request_id = httpContext.get('requestId')
|
||||
if (httpContext.get('requestPath')) info.request_path = httpContext.get('requestPath')
|
||||
if (httpContext.get('requestIp')) info.request_ip = httpContext.get('requestIp')
|
||||
return info
|
||||
})
|
||||
|
||||
const logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: format.combine(
|
||||
addRequestData(),
|
||||
anonymize(),
|
||||
format.timestamp(),
|
||||
format.json()
|
||||
|
|
|
@ -28,19 +28,16 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import apiRouter3 from '../api/v3/index.js'
|
||||
import apiRouter0 from '../api/v0/index.js'
|
||||
import apiRouter1 from '../api/v1/index.js'
|
||||
import apiRouter2 from '../api/v2/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/0', (req, res) => {
|
||||
return res.status(501).send('Proxy v0 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
|
||||
})
|
||||
router.get('/1', (req, res) => {
|
||||
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
|
||||
})
|
||||
router.get('/2', (req, res) => {
|
||||
return res.status(501).send('Proxy v2 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
|
||||
})
|
||||
router.use('/3', apiRouter3)
|
||||
if ((process.env.ENABLE_MAIN_MODULE ?? 'true') === 'true') {
|
||||
router.use('/0', apiRouter0)
|
||||
}
|
||||
router.use('/1', apiRouter1)
|
||||
router.use('/2', apiRouter2)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -30,23 +30,31 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
import express from 'express'
|
||||
import markdownImport from 'markdown-it'
|
||||
import { readFileSync } from 'fs'
|
||||
import { getMetaFromReq } from '../server/utils.js'
|
||||
import demoData from '../server/demo.js'
|
||||
|
||||
const router = express.Router()
|
||||
const md = markdownImport({ typographer: true })
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.render('index', { meta: getMetaFromReq(req) })
|
||||
})
|
||||
const highlights = []
|
||||
for (let index = 1; index < 4; index++) {
|
||||
if (process.env[`KX_HIGHLIGHTS_${index}_NAME`] &&
|
||||
process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) {
|
||||
highlights.push({
|
||||
name: process.env[`KX_HIGHLIGHTS_${index}_NAME`],
|
||||
description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`],
|
||||
fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/apps', (req, res) => {
|
||||
res.render('apps', { title: 'Apps', meta: getMetaFromReq(req) })
|
||||
res.render('index', { highlights, demoData })
|
||||
})
|
||||
|
||||
router.get('/privacy', (req, res) => {
|
||||
const rawContent = readFileSync('./content/privacy-policy.md', 'utf8')
|
||||
const content = md.render(rawContent)
|
||||
res.render('article', { title: 'Privacy policy', content, meta: getMetaFromReq(req) })
|
||||
res.render('article', { title: 'Privacy policy', content })
|
||||
})
|
||||
|
||||
router.get('/.well-known/webfinger', (req, res) => {
|
||||
|
@ -67,12 +75,6 @@ router.get('/.well-known/webfinger', (req, res) => {
|
|||
res.json(body)
|
||||
})
|
||||
|
||||
router.get('/.well-known/keyoxide/version', async (req, res) => {
|
||||
// TODO Support responding with JSON object when requested
|
||||
const meta = getMetaFromReq(req)
|
||||
return res.status(200).contentType('text/plain').send(meta.keyoxide.semver)
|
||||
})
|
||||
|
||||
router.get('/users/keyoxide', (req, res) => {
|
||||
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
||||
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')
|
||||
|
|
|
@ -29,143 +29,87 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
*/
|
||||
import express from 'express'
|
||||
import bodyParserImport from 'body-parser'
|
||||
import { rateLimit } from 'express-rate-limit'
|
||||
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
|
||||
import { Profile } from 'doipjs'
|
||||
import { generateProfileTheme, getMetaFromReq, escapedParam } from '../server/utils.js'
|
||||
import logger from '../log.js'
|
||||
|
||||
const router = express.Router()
|
||||
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
||||
|
||||
let profileRateLimiter = (req, res, next) => {
|
||||
next()
|
||||
}
|
||||
router.get('/sig', (req, res) => {
|
||||
res.render('profile', { isSignature: true, signature: null })
|
||||
})
|
||||
|
||||
if (process.env.ENABLE_EXPERIMENTAL_RATE_LIMITER) {
|
||||
profileRateLimiter = rateLimit({
|
||||
windowMs: 5000,
|
||||
limit: 20,
|
||||
standardHeaders: 'draft-7',
|
||||
legacyHeaders: false,
|
||||
handler: (req, res, next, options) => {
|
||||
logger.debug('Rate-limiting a profile request',
|
||||
{ component: 'profile_rate_limiter', action: 'block' })
|
||||
|
||||
res.status(options.statusCode).render('429', { meta: getMetaFromReq(req) })
|
||||
}
|
||||
router.post('/sig', bodyParser, async (req, res) => {
|
||||
const data = await generateSignatureProfile(req.body.signature)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
isSignature: true,
|
||||
signature: req.body.signature,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
})
|
||||
|
||||
logger.debug('Starting the profile request rate limiter',
|
||||
{ component: 'profile_rate_limiter', action: 'start' })
|
||||
}
|
||||
|
||||
router.get('/sig',
|
||||
profileRateLimiter,
|
||||
(req, res) => {
|
||||
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
|
||||
router.get('/wkd/:id', async (req, res) => {
|
||||
const data = await generateWKDProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
})
|
||||
|
||||
router.post('/sig',
|
||||
profileRateLimiter,
|
||||
bodyParser,
|
||||
async (req, res) => {
|
||||
const data = await generateSignatureProfile(req.body.signature)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
isSignature: true,
|
||||
signature: req.body.signature,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false,
|
||||
meta: getMetaFromReq(req)
|
||||
})
|
||||
router.get('/hkp/:id', async (req, res) => {
|
||||
const data = await generateHKPProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/wkd/:id',
|
||||
profileRateLimiter,
|
||||
escapedParam('id'),
|
||||
async (req, res) => {
|
||||
const data = await generateWKDProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false,
|
||||
meta: getMetaFromReq(req)
|
||||
})
|
||||
router.get('/hkp/:server/:id', async (req, res) => {
|
||||
const data = await generateHKPProfile(req.params.id, req.params.server)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/hkp/:id',
|
||||
profileRateLimiter,
|
||||
escapedParam('id'),
|
||||
async (req, res) => {
|
||||
const data = await generateHKPProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false,
|
||||
meta: getMetaFromReq(req)
|
||||
})
|
||||
router.get('/keybase/:username/:fingerprint', async (req, res) => {
|
||||
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/hkp/:server/:id',
|
||||
profileRateLimiter,
|
||||
escapedParam('server'),
|
||||
escapedParam('id'),
|
||||
async (req, res) => {
|
||||
const data = await generateHKPProfile(req.params.id, req.params.server)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false,
|
||||
meta: getMetaFromReq(req)
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/keybase/:username/:fingerprint',
|
||||
profileRateLimiter,
|
||||
escapedParam('username'),
|
||||
escapedParam('fingerprint'),
|
||||
async (req, res) => {
|
||||
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false,
|
||||
meta: getMetaFromReq(req)
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/:id',
|
||||
profileRateLimiter,
|
||||
escapedParam('id'),
|
||||
async (req, res) => {
|
||||
const data = await generateAutoProfile(req.params.id)
|
||||
const theme = generateProfileTheme(data)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false,
|
||||
theme,
|
||||
meta: getMetaFromReq(req)
|
||||
})
|
||||
router.get('/:id', async (req, res) => {
|
||||
const data = await generateAutoProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
@ -28,65 +28,52 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { escapedParam, getMetaFromReq } from '../server/utils.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', function (req, res) {
|
||||
res.render('util/index', { meta: getMetaFromReq(req) })
|
||||
res.render('util/index')
|
||||
})
|
||||
router.get('/profile-url', function (req, res) {
|
||||
res.render('util/profile-url', { meta: getMetaFromReq(req) })
|
||||
res.render('util/profile-url')
|
||||
})
|
||||
router.get('/profile-url/:input', function (req, res) {
|
||||
res.render('util/profile-url', { input: req.params.input })
|
||||
})
|
||||
router.get('/profile-url/:input',
|
||||
escapedParam('input'),
|
||||
function (req, res) {
|
||||
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||
})
|
||||
|
||||
router.get('/qr', function (req, res) {
|
||||
res.render('util/qr', { meta: getMetaFromReq(req) })
|
||||
res.render('util/qr')
|
||||
})
|
||||
router.get('/qr/:input', function (req, res) {
|
||||
res.render('util/qr', { input: req.params.input })
|
||||
})
|
||||
router.get('/qr/:input',
|
||||
escapedParam('input'),
|
||||
function (req, res) {
|
||||
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||
})
|
||||
|
||||
router.get('/qrfp', function (req, res) {
|
||||
res.render('util/qrfp', { meta: getMetaFromReq(req) })
|
||||
res.render('util/qrfp')
|
||||
})
|
||||
router.get('/qrfp/:input', function (req, res) {
|
||||
res.render('util/qrfp', { input: req.params.input })
|
||||
})
|
||||
router.get('/qrfp/:input',
|
||||
escapedParam('input'),
|
||||
function (req, res) {
|
||||
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||
})
|
||||
|
||||
router.get('/wkd', function (req, res) {
|
||||
res.render('util/wkd', { meta: getMetaFromReq(req) })
|
||||
res.render('util/wkd')
|
||||
})
|
||||
router.get('/wkd/:input', function (req, res) {
|
||||
res.render('util/wkd', { input: req.params.input })
|
||||
})
|
||||
router.get('/wkd/:input',
|
||||
escapedParam('input'),
|
||||
function (req, res) {
|
||||
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||
})
|
||||
|
||||
router.get('/argon2', function (req, res) {
|
||||
res.render('util/argon2', { meta: getMetaFromReq(req) })
|
||||
res.render('util/argon2')
|
||||
})
|
||||
router.get('/argon2/:input', function (req, res) {
|
||||
res.render('util/argon2', { input: req.params.input })
|
||||
})
|
||||
router.get('/argon2/:input',
|
||||
escapedParam('input'),
|
||||
function (req, res) {
|
||||
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||
})
|
||||
|
||||
router.get('/bcrypt', function (req, res) {
|
||||
res.render('util/bcrypt', { meta: getMetaFromReq(req) })
|
||||
res.render('util/bcrypt')
|
||||
})
|
||||
router.get('/bcrypt/:input', function (req, res) {
|
||||
res.render('util/bcrypt', { input: req.params.input })
|
||||
})
|
||||
router.get('/bcrypt/:input',
|
||||
escapedParam('input'),
|
||||
function (req, res) {
|
||||
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
387
src/schemas.js
387
src/schemas.js
|
@ -1,387 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const profileSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/profile.schema.json',
|
||||
title: 'Profile',
|
||||
description: 'Keyoxide profile with personas',
|
||||
type: 'object',
|
||||
properties: {
|
||||
profileVersion: {
|
||||
description: 'The version of the profile',
|
||||
type: 'integer'
|
||||
},
|
||||
profileType: {
|
||||
description: 'The type of the profile [openpgp, asp]',
|
||||
type: 'string'
|
||||
},
|
||||
identifier: {
|
||||
description: 'Identifier of the profile (email, fingerprint, URI)',
|
||||
type: 'string'
|
||||
},
|
||||
personas: {
|
||||
description: 'The personas inside the profile',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: 'https://spec.keyoxide.org/2/persona.schema.json'
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
primaryPersonaIndex: {
|
||||
description: 'The index of the primary persona',
|
||||
type: 'integer'
|
||||
},
|
||||
publicKey: {
|
||||
description: 'The cryptographic key associated with the profile',
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyType: {
|
||||
description: 'The type of cryptographic key [eddsa, es256, openpgp, none]',
|
||||
type: 'string'
|
||||
},
|
||||
encoding: {
|
||||
description: 'The encoding of the cryptographic key [pem, jwk, armored_pgp, none]',
|
||||
type: 'string'
|
||||
},
|
||||
encodedKey: {
|
||||
description: 'The encoded cryptographic key (PEM, stringified JWK, ...)',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
fetch: {
|
||||
description: 'Details on how to fetch the public key',
|
||||
type: 'object',
|
||||
properties: {
|
||||
method: {
|
||||
description: 'The method to fetch the key [aspe, hkp, wkd, http, none]',
|
||||
type: 'string'
|
||||
},
|
||||
query: {
|
||||
description: 'The query to fetch the key',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
resolvedUrl: {
|
||||
description: 'The URL the method eventually resolved to',
|
||||
type: ['string', 'null']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'keyType',
|
||||
'fetch'
|
||||
]
|
||||
},
|
||||
verifiers: {
|
||||
description: 'A list of links to verifiers',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
description: 'Name of the verifier site',
|
||||
type: 'string'
|
||||
},
|
||||
url: {
|
||||
description: 'URL to the profile page on the verifier site',
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'profileVersion',
|
||||
'profileType',
|
||||
'identifier',
|
||||
'personas',
|
||||
'primaryPersonaIndex',
|
||||
'publicKey',
|
||||
'verifiers'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
export const personaSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/persona.schema.json',
|
||||
title: 'Profile',
|
||||
description: 'Keyoxide persona with identity claims',
|
||||
type: 'object',
|
||||
properties: {
|
||||
identifier: {
|
||||
description: 'Identifier of the persona',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
name: {
|
||||
description: 'Name of the persona',
|
||||
type: 'string'
|
||||
},
|
||||
email: {
|
||||
description: 'Email address of the persona',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
description: {
|
||||
description: 'Description of the persona',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
avatarUrl: {
|
||||
description: 'URL to an avatar image',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
themeColor: {
|
||||
description: 'Profile page theme color',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
isRevoked: {
|
||||
type: 'boolean'
|
||||
},
|
||||
claims: {
|
||||
description: 'A list of identity claims',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: 'https://spec.keyoxide.org/2/claim.schema.json'
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
'claims'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
export const claimSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/claim.schema.json',
|
||||
title: 'Identity claim',
|
||||
description: 'Verifiable online identity claim',
|
||||
type: 'object',
|
||||
properties: {
|
||||
claimVersion: {
|
||||
description: 'The version of the claim',
|
||||
type: 'integer'
|
||||
},
|
||||
uri: {
|
||||
description: 'The claim URI',
|
||||
type: 'string'
|
||||
},
|
||||
proofs: {
|
||||
description: 'The proofs that would verify the claim',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
matches: {
|
||||
description: 'Service providers matched to the claim',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: 'https://spec.keyoxide.org/2/serviceprovider.schema.json'
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
status: {
|
||||
type: 'integer',
|
||||
description: 'Claim status code'
|
||||
},
|
||||
display: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
profileName: {
|
||||
type: 'string',
|
||||
description: 'Account name to display in the user interface'
|
||||
},
|
||||
profileUrl: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Profile URL to link to in the user interface'
|
||||
},
|
||||
proofUrl: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Proof URL to link to in the user interface'
|
||||
},
|
||||
serviceProviderName: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Name of the service provider to display in the user interface'
|
||||
},
|
||||
serviceProviderId: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Id of the service provider'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'claimVersion',
|
||||
'uri',
|
||||
'proofs',
|
||||
'status',
|
||||
'display'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
export const serviceProviderSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/serviceprovider.schema.json',
|
||||
title: 'Service provider',
|
||||
description: 'A service provider that can be matched to identity claims',
|
||||
type: 'object',
|
||||
properties: {
|
||||
about: {
|
||||
description: 'Details about the service provider',
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
description: 'Full name of the service provider',
|
||||
type: 'string'
|
||||
},
|
||||
id: {
|
||||
description: 'Identifier of the service provider (no whitespace or symbols, lowercase)',
|
||||
type: 'string'
|
||||
},
|
||||
homepage: {
|
||||
description: 'URL to the homepage of the service provider',
|
||||
type: ['string', 'null']
|
||||
}
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
description: 'What the profile would look like if the match is correct',
|
||||
type: 'object',
|
||||
properties: {
|
||||
display: {
|
||||
description: 'Profile name to be displayed',
|
||||
type: 'string'
|
||||
},
|
||||
uri: {
|
||||
description: 'URI or URL for public access to the profile',
|
||||
type: 'string'
|
||||
},
|
||||
qr: {
|
||||
description: 'URI or URL associated with the profile usually served as a QR code',
|
||||
type: ['string', 'null']
|
||||
}
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
description: 'Details from the claim matching process',
|
||||
type: 'object',
|
||||
properties: {
|
||||
uriRegularExpression: {
|
||||
description: 'Regular expression used to parse the URI',
|
||||
type: 'string'
|
||||
},
|
||||
uriIsAmbiguous: {
|
||||
description: 'Whether this match automatically excludes other matches',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
},
|
||||
proof: {
|
||||
description: 'Information for the proof verification process',
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
description: 'Details to request the potential proof',
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: {
|
||||
description: 'Location of the proof',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
accessRestriction: {
|
||||
description: 'Type of access restriction [none, nocors, granted, server]',
|
||||
type: 'string'
|
||||
},
|
||||
fetcher: {
|
||||
description: 'Name of the fetcher to use',
|
||||
type: 'string'
|
||||
},
|
||||
data: {
|
||||
description: 'Data needed by the fetcher or proxy to request the proof',
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
description: 'Details about the expected response',
|
||||
type: 'object',
|
||||
properties: {
|
||||
format: {
|
||||
description: 'Expected format of the proof [text, json]',
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
target: {
|
||||
description: 'Details about the target located in the response',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
format: {
|
||||
description: 'How is the proof formatted [uri, fingerprint]',
|
||||
type: 'string'
|
||||
},
|
||||
encoding: {
|
||||
description: 'How is the proof encoded [plain, html, xml]',
|
||||
type: 'string'
|
||||
},
|
||||
relation: {
|
||||
description: 'How are the response and the target related [contains, equals]',
|
||||
type: 'string'
|
||||
},
|
||||
path: {
|
||||
description: 'Path to the target location if the response is JSON',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'about',
|
||||
'profile',
|
||||
'claim',
|
||||
'proof'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
|
@ -27,76 +27,55 @@ You should also get your employer (if you work as a programmer) or school,
|
|||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@use './styles/vars.scss';
|
||||
@use './styles/layout.scss';
|
||||
@use './styles/typography.scss';
|
||||
@use './styles/forms.scss';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:focus {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* HELPERS */
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
display: block;
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
#qr {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
max-width: 256px !important;
|
||||
height: auto !important;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
/* DIALOGS */
|
||||
dialog {
|
||||
max-width: 480px;
|
||||
word-wrap: anywhere;
|
||||
background-color: var(--section-background-color);
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
|
||||
&::backdrop {
|
||||
background-color: #000;
|
||||
opacity: 0.8;
|
||||
export default {
|
||||
claimVersion: 1,
|
||||
uri: 'https://fosstodon.org/@keyoxide',
|
||||
fingerprint: '9f0048ac0b23301e1f77e994909f6bd6f80f485d',
|
||||
status: 'verified',
|
||||
matches: [
|
||||
{
|
||||
serviceprovider: {
|
||||
type: 'web',
|
||||
name: 'mastodon (demo)'
|
||||
},
|
||||
match: {
|
||||
regularExpression: {},
|
||||
isAmbiguous: true
|
||||
},
|
||||
profile: {
|
||||
display: '@keyoxide@fosstodon.org',
|
||||
uri: 'https://fosstodon.org/@keyoxide',
|
||||
qr: null
|
||||
},
|
||||
proof: {
|
||||
uri: 'https://fosstodon.org/@keyoxide',
|
||||
request: {
|
||||
fetcher: 'http',
|
||||
access: 0,
|
||||
format: 'json',
|
||||
data: {
|
||||
url: 'https://fosstodon.org/@keyoxide',
|
||||
format: 'json'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
format: 1,
|
||||
relation: 0,
|
||||
path: [
|
||||
'attachment',
|
||||
'value'
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
verification: {
|
||||
result: true,
|
||||
completed: true,
|
||||
errors: [],
|
||||
proof: {
|
||||
fetcher: 'http',
|
||||
viaProxy: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog form[method="Dialog"] {
|
||||
margin: 1em 0 0 !important;
|
||||
}
|
||||
|
||||
dialog form[method="Dialog"] input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
dialog p {
|
||||
font-size: 1rem !important;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
dialog p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
|
@ -29,52 +29,45 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
*/
|
||||
import logger from '../log.js'
|
||||
import * as doipjs from 'doipjs'
|
||||
import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './openpgpProfiles.js'
|
||||
import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './keys.js'
|
||||
import libravatar from 'libravatar'
|
||||
|
||||
const generateAspeProfile = async (id) => {
|
||||
logger.debug('Generating an ASPE profile',
|
||||
{ component: 'aspe_profile_generator', action: 'start', profile_id: id })
|
||||
|
||||
return doipjs.asp.fetchASPE(id)
|
||||
.then(profile => {
|
||||
if (process.env.DOMAIN) {
|
||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
|
||||
}
|
||||
profile = processAspProfile(profile)
|
||||
return profile
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate ASPE profile',
|
||||
{ component: 'aspe_profile_generator', action: 'failure', error: err.message, profile_id: id })
|
||||
|
||||
return {
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const generateWKDProfile = async (id) => {
|
||||
logger.debug('Generating a WKD profile',
|
||||
{ component: 'wkd_profile_generator', action: 'start', profile_id: id })
|
||||
|
||||
return fetchWKD(id)
|
||||
.then(async profile => {
|
||||
if (process.env.DOMAIN) {
|
||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
|
||||
}
|
||||
profile = processOpenPgpProfile(profile)
|
||||
.then(async key => {
|
||||
let keyData = await doipjs.keys.process(key.publicKey)
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
keyData.key.fetchMethod = 'wkd'
|
||||
keyData.key.uri = key.fetchURL
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
keyoxideData.url = `https://${process.env.DOMAIN}/wkd/${id}`
|
||||
|
||||
logger.debug('Generating a WKD profile',
|
||||
{ component: 'wkd_profile_generator', action: 'done', profile_id: id })
|
||||
|
||||
return profile
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate WKD profile',
|
||||
{ component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
|
@ -85,29 +78,41 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
|||
{ component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
return fetchHKP(id, keyserverDomain)
|
||||
.then(async profile => {
|
||||
let keyoxideUrl
|
||||
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
|
||||
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}`
|
||||
} else {
|
||||
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||
}
|
||||
.then(async key => {
|
||||
let keyData = await doipjs.keys.process(key.publicKey)
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
keyData.key.fetchMethod = 'hkp'
|
||||
keyData.key.uri = key.fetchURL
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
if (process.env.DOMAIN) {
|
||||
profile.addVerifier('keyoxide', keyoxideUrl)
|
||||
const keyoxideData = {}
|
||||
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
|
||||
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${id}`
|
||||
} else {
|
||||
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||
}
|
||||
profile = processOpenPgpProfile(profile)
|
||||
|
||||
logger.debug('Generating a HKP profile',
|
||||
{ component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
return profile
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate HKP profile',
|
||||
{ component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
|
@ -116,41 +121,25 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
|||
const generateAutoProfile = async (id) => {
|
||||
let result
|
||||
|
||||
const aspeRe = /aspe:(.*):(.*)/
|
||||
const openpgpRe = /openpgp4fpr:(.*)/
|
||||
|
||||
if (aspeRe.test(id)) {
|
||||
result = await generateAspeProfile(id)
|
||||
|
||||
if (result && !('errors' in result)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (openpgpRe.test(id)) {
|
||||
const match = id.match(openpgpRe)
|
||||
result = await generateHKPProfile(match[1])
|
||||
|
||||
if (result && !('errors' in result)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (id.includes('@')) {
|
||||
result = await generateWKDProfile(id)
|
||||
|
||||
if (result && !('errors' in result)) {
|
||||
if (result && result.errors.length === 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result = await generateHKPProfile(id)
|
||||
if (result && !('errors' in result)) {
|
||||
if (result && result.errors.length === 0) {
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
errors: ['No public profile/keys could be found']
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: ['No public keys could be found']
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,19 +149,34 @@ const generateSignatureProfile = async (signature) => {
|
|||
|
||||
return fetchSignature(signature)
|
||||
.then(async key => {
|
||||
let profile = await doipjs.signatures.parse(key.publicKey)
|
||||
profile = processOpenPgpProfile(profile)
|
||||
let keyData = key.keyData
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
delete key.keyData
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
|
||||
logger.debug('Generating a signature profile',
|
||||
{ component: 'signature_profile_generator', action: 'done' })
|
||||
|
||||
return profile
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate a signature profile',
|
||||
{ component: 'signature_profile_generator', action: 'failure', error: err.message })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
|
@ -183,105 +187,82 @@ const generateKeybaseProfile = async (username, fingerprint) => {
|
|||
{ component: 'keybase_profile_generator', action: 'start', username, fingerprint })
|
||||
|
||||
return fetchKeybase(username, fingerprint)
|
||||
.then(async profile => {
|
||||
if (process.env.DOMAIN) {
|
||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
|
||||
}
|
||||
profile = processOpenPgpProfile(profile)
|
||||
.then(async key => {
|
||||
let keyData = await doipjs.keys.process(key.publicKey)
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
keyData.key.fetchMethod = 'hkp'
|
||||
keyData.key.uri = key.fetchURL
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
keyoxideData.url = `https://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`
|
||||
|
||||
logger.debug('Generating a Keybase profile',
|
||||
{ component: 'keybase_profile_generator', action: 'done', username, fingerprint })
|
||||
|
||||
return profile
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate a Keybase profile',
|
||||
{ component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const processAspProfile = async (/** @type {import('doipjs').Profile */ profile) => {
|
||||
profile.personas.forEach(persona => {
|
||||
const processKeyData = (keyData) => {
|
||||
keyData.users.forEach(user => {
|
||||
// Remove faulty claims
|
||||
persona.claims = persona.claims.filter(claim => {
|
||||
user.claims = user.claims.filter(claim => {
|
||||
return claim instanceof doipjs.Claim
|
||||
})
|
||||
|
||||
// Match claims
|
||||
persona.claims.forEach(claim => {
|
||||
user.claims.forEach(claim => {
|
||||
claim.match()
|
||||
})
|
||||
|
||||
// Sort claims
|
||||
persona.claims.sort((a, b) => {
|
||||
user.claims.sort((a, b) => {
|
||||
if (a.matches.length === 0) return 1
|
||||
if (b.matches.length === 0) return -1
|
||||
|
||||
if (a.matches[0].about.name < b.matches[0].about.name) {
|
||||
if (a.matches[0].serviceprovider.name < b.matches[0].serviceprovider.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.matches[0].about.name > b.matches[0].about.name) {
|
||||
if (a.matches[0].serviceprovider.name > b.matches[0].serviceprovider.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
})
|
||||
|
||||
// Overwrite avatarUrl
|
||||
// TODO: don't overwrite avatarUrl once it's fully supported
|
||||
profile.personas[profile.primaryPersonaIndex].avatarUrl =
|
||||
`https://${process.env.DICEBEAR_API_HOSTNAME || 'api.dicebear.com'}/7.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
|
||||
|
||||
return profile
|
||||
return keyData
|
||||
}
|
||||
|
||||
const processOpenPgpProfile = async (/** @type {import('doipjs').Profile */ profile) => {
|
||||
profile.personas.forEach(persona => {
|
||||
// Remove faulty claims
|
||||
persona.claims = persona.claims.filter(claim => {
|
||||
return claim instanceof doipjs.Claim
|
||||
})
|
||||
const computeExtraData = async (key, keyData) => {
|
||||
// Get the primary user
|
||||
const primaryUser = await key.publicKey.getPrimaryUser()
|
||||
|
||||
// Match claims
|
||||
persona.claims.forEach(claim => {
|
||||
claim.match()
|
||||
})
|
||||
|
||||
// Sort claims
|
||||
persona.claims.sort((a, b) => {
|
||||
if (a.matches.length === 0) return 1
|
||||
if (b.matches.length === 0) return -1
|
||||
|
||||
if (a.matches[0].about.name < b.matches[0].about.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.matches[0].about.name > b.matches[0].about.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
})
|
||||
|
||||
// Overwrite avatarUrl
|
||||
// TODO: don't overwrite avatarUrl once it's fully supported
|
||||
profile.personas[profile.primaryPersonaIndex].avatarUrl = await libravatar.get_avatar_url({ email: profile.personas[profile.primaryPersonaIndex].email, size: 128, default: 'mm', https: true })
|
||||
|
||||
return profile
|
||||
// Query libravatar to get the avatar url
|
||||
return {
|
||||
avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true })
|
||||
}
|
||||
}
|
||||
|
||||
const getScheme = () => {
|
||||
return process.env.PROXY_SCHEME
|
||||
? process.env.PROXY_SCHEME
|
||||
: process.env.SCHEME
|
||||
? process.env.SCHEME
|
||||
: 'https'
|
||||
}
|
||||
|
||||
export { generateAspeProfile }
|
||||
export { generateWKDProfile }
|
||||
export { generateHKPProfile }
|
||||
export { generateAutoProfile }
|
||||
|
|
243
src/server/keys.js
Normal file
243
src/server/keys.js
Normal file
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import got from 'got'
|
||||
import * as doipjs from 'doipjs'
|
||||
import { readKey, readCleartextMessage, verify, PublicKey } from 'openpgp'
|
||||
import { computeWKDLocalPart } from './utils.js'
|
||||
import { createHash } from 'crypto'
|
||||
import Keyv from 'keyv'
|
||||
|
||||
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
|
||||
|
||||
const fetchWKD = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
|
||||
if (!id.includes('@')) {
|
||||
reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||
}
|
||||
|
||||
const [, localPart, domain] = /([^@]*)@(.*)/.exec(id)
|
||||
if (!localPart || !domain) {
|
||||
reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||
}
|
||||
const localEncoded = await computeWKDLocalPart(localPart)
|
||||
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
|
||||
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
|
||||
let plaintext
|
||||
|
||||
const hash = createHash('md5').update(id).digest('hex')
|
||||
if (c && await c.get(hash)) {
|
||||
plaintext = Uint8Array.from((await c.get(hash)).split(','))
|
||||
}
|
||||
|
||||
if (!plaintext) {
|
||||
try {
|
||||
plaintext = await got(urlAdvanced).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
output.fetchURL = urlAdvanced
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
try {
|
||||
plaintext = await got(urlDirect).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
output.fetchURL = urlDirect
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be fetched using WKD'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!plaintext) {
|
||||
reject(new Error('No public keys could be fetched using WKD'))
|
||||
}
|
||||
|
||||
if (c && plaintext instanceof Uint8Array) {
|
||||
await c.set(hash, plaintext.toString(), 60 * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
output.publicKey = await readKey({
|
||||
binaryKey: plaintext
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchHKP = (id, keyserverDomain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
|
||||
keyserverDomain = keyserverDomain || 'keys.openpgp.org'
|
||||
|
||||
let query = ''
|
||||
if (id.includes('@')) {
|
||||
query = id
|
||||
} else {
|
||||
let sanitizedId = id
|
||||
const whitespaceRegex = /\s/g
|
||||
if (whitespaceRegex.test(id)) {
|
||||
sanitizedId = id.replaceAll(whitespaceRegex, '')
|
||||
}
|
||||
query = `0x${sanitizedId}`
|
||||
}
|
||||
|
||||
output.fetchURL = `https://${keyserverDomain}/pks/lookup?op=get&options=mr&search=${query}`
|
||||
|
||||
const hash = createHash('md5').update(`${query}__${keyserverDomain}`).digest('hex')
|
||||
|
||||
if (c && await c.get(hash)) {
|
||||
output.publicKey = await readKey({
|
||||
armoredKey: await c.get(hash)
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
output.publicKey = await doipjs.keys.fetchHKP(query, keyserverDomain)
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be fetched using HKP'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be fetched using HKP'))
|
||||
}
|
||||
|
||||
if (c && output.publicKey instanceof PublicKey) {
|
||||
await c.set(hash, output.publicKey.armor(), 60 * 1000)
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchSignature = (signature) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null,
|
||||
keyData: null
|
||||
}
|
||||
|
||||
// Check validity of signature
|
||||
let signatureData
|
||||
try {
|
||||
signatureData = await readCleartextMessage({
|
||||
cleartextMessage: signature
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error(`Signature could not be properly read (${error.message})`))
|
||||
}
|
||||
|
||||
// Process the signature
|
||||
try {
|
||||
output.keyData = await doipjs.signatures.process(signature)
|
||||
output.publicKey = output.keyData.key.data
|
||||
// TODO Find the URL to the key
|
||||
output.fetchURL = null
|
||||
} catch (error) {
|
||||
reject(new Error(`Signature could not be properly read (${error.message})`))
|
||||
}
|
||||
|
||||
// Check if a key was fetched
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be fetched'))
|
||||
}
|
||||
|
||||
// Check validity of signature
|
||||
const verified = await verify({
|
||||
message: signatureData,
|
||||
verificationKeys: output.publicKey
|
||||
})
|
||||
|
||||
if (!await verified.signatures[0].verified) {
|
||||
reject(new Error('Signature was invalid'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchKeybase = (username, fingerprint) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
|
||||
try {
|
||||
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint)
|
||||
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
export { fetchWKD }
|
||||
export { fetchHKP }
|
||||
export { fetchSignature }
|
||||
export { fetchKeybase }
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import logger from '../log.js'
|
||||
import got from 'got'
|
||||
import * as doipjs from 'doipjs'
|
||||
import { readKey } from 'openpgp'
|
||||
import { computeWKDLocalPart } from './utils.js'
|
||||
import { createHash } from 'crypto'
|
||||
import Keyv from 'keyv'
|
||||
|
||||
let c = null
|
||||
if (process.env.ENABLE_EXPERIMENTAL_CACHE) {
|
||||
c = new Keyv()
|
||||
|
||||
logger.debug('OpenPGP cache started',
|
||||
{ component: 'openpgp_cache', action: 'start' })
|
||||
}
|
||||
|
||||
const fetchWKD = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
logger.debug('Fetching an OpenPGP profile via WKD',
|
||||
{ component: 'wkd_profile_fetcher', action: 'start', profile_id: id })
|
||||
|
||||
let publicKey = null
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
if (!id.includes('@')) {
|
||||
return reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||
}
|
||||
|
||||
const [, localPart, domain] = /([^@]*)@(.*)/.exec(id)
|
||||
if (!(localPart && domain)) {
|
||||
return reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||
}
|
||||
const localEncoded = await computeWKDLocalPart(localPart)
|
||||
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
|
||||
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
|
||||
let plaintext
|
||||
|
||||
const hash = createHash('md5').update(id).digest('hex')
|
||||
if (c && await c.get(hash)) {
|
||||
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
|
||||
|
||||
logger.debug('WKD profile retrieved from OpenPGP cache',
|
||||
{ component: 'openpgp_cache', action: 'retrieve_wkd' })
|
||||
|
||||
return resolve(profile)
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
try {
|
||||
plaintext = await got(urlAdvanced).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
fetchURL = urlAdvanced
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
} catch (errorAdvanced) {
|
||||
logger.debug('Failed to fetch an OpenPGP profile via WKD (advanced URL)',
|
||||
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorAdvanced.message })
|
||||
|
||||
try {
|
||||
plaintext = await got(urlDirect).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
fetchURL = urlDirect
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
} catch (errorDirect) {
|
||||
logger.debug('Failed to fetch an OpenPGP profile via WKD (direct URL)',
|
||||
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorDirect.message })
|
||||
|
||||
return reject(new Error('No public keys could be fetched using WKD'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!plaintext) {
|
||||
return reject(new Error('No public keys could be fetched using WKD'))
|
||||
}
|
||||
|
||||
try {
|
||||
publicKey = await readKey({
|
||||
binaryKey: plaintext
|
||||
})
|
||||
} catch (error) {
|
||||
logger.debug('Failed to fetch an OpenPGP profile via WKD (reading key)',
|
||||
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
|
||||
|
||||
return reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
if (!publicKey) {
|
||||
return reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
try {
|
||||
profile = await doipjs.openpgp.parsePublicKey(publicKey)
|
||||
} catch (error) {
|
||||
logger.debug('Failed to fetch an OpenPGP profile via WKD (parsing key)',
|
||||
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
|
||||
|
||||
return reject(new Error('No public keys could be fetched using WKD'))
|
||||
}
|
||||
|
||||
profile.publicKey.fetch.method = 'wkd'
|
||||
profile.publicKey.fetch.query = id
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
}
|
||||
|
||||
if (c && plaintext instanceof Uint8Array) {
|
||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||
|
||||
logger.debug('WKD profile stored in OpenPGP cache',
|
||||
{ component: 'openpgp_cache', action: 'store_wkd' })
|
||||
}
|
||||
|
||||
logger.debug('Fetched an OpenPGP profile via WKD',
|
||||
{ component: 'wkd_profile_fetcher', action: 'done', profile_id: id })
|
||||
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchHKP = (id, keyserverDomain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
logger.debug('Fetching an OpenPGP profile via HKP',
|
||||
{ component: 'hkp_profile_fetcher', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org'
|
||||
|
||||
let query = ''
|
||||
if (id.includes('@')) {
|
||||
query = id
|
||||
} else {
|
||||
let sanitizedId = id
|
||||
const whitespaceRegex = /\s/g
|
||||
if (whitespaceRegex.test(id)) {
|
||||
sanitizedId = id.replaceAll(whitespaceRegex, '')
|
||||
}
|
||||
query = `0x${sanitizedId}`
|
||||
}
|
||||
|
||||
fetchURL = `https://${keyserverDomainNormalized}/pks/lookup?op=get&options=mr&search=${query}`
|
||||
|
||||
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
|
||||
|
||||
if (c && await c.get(hash)) {
|
||||
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
|
||||
|
||||
logger.debug('HKP profile retrieved from OpenPGP cache',
|
||||
{ component: 'openpgp_cache', action: 'store_hkp' })
|
||||
|
||||
return resolve(profile)
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
try {
|
||||
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
|
||||
} catch (error) {
|
||||
logger.debug('Failed to fetch an OpenPGP profile via HKP',
|
||||
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, keyserver_domain: keyserverDomain || '', error: error.message })
|
||||
|
||||
profile = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return reject(new Error('No public keys could be fetched using HKP'))
|
||||
}
|
||||
|
||||
profile.publicKey.fetch.method = 'hkp'
|
||||
profile.publicKey.fetch.query = id
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
|
||||
if (c && profile instanceof doipjs.Profile) {
|
||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||
|
||||
logger.debug('HKP profile stored in OpenPGP cache',
|
||||
{ component: 'openpgp_cache', action: 'store_hkp' })
|
||||
}
|
||||
|
||||
logger.debug('Fetched an OpenPGP profile via HKP',
|
||||
{ component: 'hkp_profile_fetcher', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchSignature = (signature) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
let profile = null
|
||||
|
||||
// Process the signature
|
||||
try {
|
||||
profile = await doipjs.signatures.parse(signature)
|
||||
// TODO Find the URL to the key
|
||||
} catch (error) {
|
||||
return reject(new Error(`Signature could not be properly read (${error.message})`))
|
||||
}
|
||||
|
||||
// Check if a key was fetched
|
||||
if (!profile) {
|
||||
return reject(new Error('No profile could be fetched'))
|
||||
}
|
||||
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
const fetchKeybase = (username, fingerprint) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
try {
|
||||
profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
|
||||
fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
||||
} catch (error) {
|
||||
return reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
profile.publicKey.fetch.method = 'http'
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
export { fetchWKD }
|
||||
export { fetchHKP }
|
||||
export { fetchSignature }
|
||||
export { fetchKeybase }
|
|
@ -28,9 +28,6 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { webcrypto as crypto } from 'crypto'
|
||||
import { Profile } from 'doipjs'
|
||||
import Color from 'colorjs.io'
|
||||
import { param } from 'express-validator'
|
||||
|
||||
export async function computeWKDLocalPart (localPart) {
|
||||
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
|
||||
|
@ -42,7 +39,7 @@ export function generatePageTitle (type, data) {
|
|||
switch (type) {
|
||||
case 'profile':
|
||||
try {
|
||||
return `${data.personas[data.primaryPersonaIndex].name} - Keyoxide`
|
||||
return `${data.keyData.users[data.keyData.primaryUserIndex].userData.name} - Keyoxide`
|
||||
} catch (error) {
|
||||
return 'Profile - Keyoxide'
|
||||
}
|
||||
|
@ -81,98 +78,3 @@ export function encodeZBase32 (data) {
|
|||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function getMetaFromReq (req) {
|
||||
const versionDetails = (req.app.get('git_hash'))
|
||||
? `+${req.app.get('git_hash').substring(0, 10)}`
|
||||
: ''
|
||||
|
||||
const semver = `${req.app.get('keyoxide_name')}/${req.app.get('keyoxide_version')}${versionDetails}`
|
||||
|
||||
const sourceUrl = req.app.get('git_hash')
|
||||
? `https://codeberg.org/keyoxide/keyoxide-web/src/commit/${req.app.get('git_hash')}`
|
||||
: 'https://codeberg.org/keyoxide/keyoxide-web'
|
||||
|
||||
return {
|
||||
env: req.app.get('env'),
|
||||
keyoxide: {
|
||||
name: req.app.get('keyoxide_name'),
|
||||
version: req.app.get('keyoxide_version'),
|
||||
branch: req.app.get('git_branch'),
|
||||
hash: req.app.get('git_hash'),
|
||||
semver,
|
||||
sourceUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateProfileTheme (/** @type {Profile} */ profile) {
|
||||
if (!(profile && profile instanceof Profile)) return null
|
||||
|
||||
if (!profile.personas[profile.primaryPersonaIndex].themeColor) return null
|
||||
|
||||
let base
|
||||
try {
|
||||
base = new Color(profile.personas[profile.primaryPersonaIndex].themeColor)
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (base.to('hsl').hsl[0].isNaN) return null
|
||||
if (base.to('hsl').hsl[2] === 0) return null
|
||||
|
||||
const primaryLight = base.to('hsl')
|
||||
primaryLight.hsl[2] = 40
|
||||
const primaryDark = base.to('hsl')
|
||||
primaryDark.hsl[2] = 80
|
||||
|
||||
const primarySubtleLight = base.to('hsl')
|
||||
primarySubtleLight.hsl[2] = 50
|
||||
const primarySubtleDark = base.to('hsl')
|
||||
primarySubtleDark.hsl[2] = 70
|
||||
|
||||
const backgroundLight = base.to('hsl')
|
||||
backgroundLight.hsl[2] = 98
|
||||
const backgroundDark = base.to('hsl')
|
||||
backgroundDark.hsl[1] = 20
|
||||
backgroundDark.hsl[2] = 5
|
||||
|
||||
return {
|
||||
base: base.toString({ format: 'hex' }),
|
||||
primary: {
|
||||
light: primaryLight.toString(),
|
||||
dark: primaryDark.toString()
|
||||
},
|
||||
primarySubtle: {
|
||||
light: primarySubtleLight.toString(),
|
||||
dark: primarySubtleDark.toString()
|
||||
},
|
||||
background: {
|
||||
light: backgroundLight.toString(),
|
||||
dark: backgroundDark.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reEmailLike = /(<[^\s@<>]+@[^\s@<>]+>)/
|
||||
|
||||
export function escapedParam (/** @type {String} */ name) {
|
||||
return param(name).customSanitizer(value => {
|
||||
return value.split(reEmailLike).map(token => {
|
||||
if (reEmailLike.test(token)) return token
|
||||
return escapeString(token)
|
||||
}).join('')
|
||||
})
|
||||
}
|
||||
|
||||
// Copied from https://github.com/validatorjs/validator.js/blob/b958bd7d1026a434ad3bf90064d3dcb8b775f1a9/src/lib/escapeString.js
|
||||
function escapeString (/** @type {String} */ input) {
|
||||
return (input.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\//g, '/')
|
||||
.replace(/\\/g, '\')
|
||||
.replace(/`/g, '`'))
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<svg role="image" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m14.662 2.7734c-0.86914 8.5e-6 -1.5742 0.70509-1.5742 1.5742 0 0.52528 0.26095 1.006 0.67773 1.2949 0.12007 0.083138 0.18912 0.092562 0.20508 0.22266 0.01604 0.13001-0.16211 0.25-0.16211 0.25l-3.3242 2.5859v-0.5293c0.14724-1.4561-0.83223-2.9405-2.2754-3.2793-1.719-0.502-3.7931 1.0298-3.7931 2.6452 0 1.9182 0.00778 8.9247 0.00211 13.429-0.00211 1.6772 1.3577 3.0332 3.0332 3.0332s3.0352-1.3576 3.0352-3.0332v-0.39648c1.365 0.90898 2.7303 1.8174 4.0938 2.7285 1.1299 0.93021 2.9078 0.93859 3.9902-0.07422 1.3712-1.1518 1.323-3.4999-0.0918-4.5957-1.935-1.3219-3.8897-2.6181-5.8418-3.916l5.7734-4.4883c1.1632-0.90466 1.4954-2.5005 0.83398-3.7832-0.04938-0.095755-0.06511-0.12998-0.16797-0.16992-0.10277-0.039938-0.18425 0.00439-0.28711 0.042969-0.14138 0.053062-0.29257 0.080078-0.44531 0.080078-0.69894 0-1.2656-0.56662-1.2656-1.2656 9.4e-5 -0.094853 0.01636-0.14996-0.01172-0.20898-0.02807-0.059023-0.10233-0.097092-0.17578-0.10547-0.12191-0.013926-0.24437-0.020476-0.36719-0.019531-0.12049 8.587e-4 -0.21169-0.00952-0.26367-0.097656-0.05189-0.088142-0.02344-0.18975-0.02344-0.34961 0-0.86915-0.70504-1.5742-1.5742-1.5742z"/>
|
||||
<path d="m12.806 3.085a1.0735 1.0735 0 0 1-1.0735 1.0735 1.0735 1.0735 0 0 1-1.0735-1.0735 1.0735 1.0735 0 0 1 1.0735-1.0735 1.0735 1.0735 0 0 1 1.0735 1.0735z"/>
|
||||
<path d="m13.458 1.0033a0.70038 0.70038 0 0 1-0.70038 0.70038 0.70038 0.70038 0 0 1-0.70038-0.70038 0.70038 0.70038 0 0 1 0.70038-0.70038 0.70038 0.70038 0 0 1 0.70038 0.70038z"/>
|
||||
<path d="m11.339 0.48902a0.48902 0.48902 0 0 1-0.48902 0.48902 0.48902 0.48902 0 0 1-0.48902-0.48902 0.48902 0.48902 0 0 1 0.48902-0.48902 0.48902 0.48902 0 0 1 0.48902 0.48902z"/>
|
||||
<path d="m19.203 5.1296a0.85797 0.85797 0 0 1-0.85797 0.85797 0.85797 0.85797 0 0 1-0.85797-0.85797 0.85797 0.85797 0 0 1 0.85797-0.85797 0.85797 0.85797 0 0 1 0.85797 0.85797z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 49 KiB |
Binary file not shown.
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
Before Width: | Height: | Size: 54 KiB |
Binary file not shown.
Before Width: | Height: | Size: 34 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m10.89 0c-0.5512 2.702e-7 -1.008 0.4571-1.008 1.008 0 0.5512 0.4571 1.008 1.008 1.008 0.2431 0 0.4661-0.0887 0.6414-0.2354 0.01238 0.05092 0.0265 0.1007 0.04512 0.1491-0.7892 0.07774-1.414 0.7551-1.414 1.563 0 0.8594 0.706 1.563 1.565 1.563 0.2774 0 0.5392-0.07366 0.767-0.202 0.05714 0.4898 0.3658 0.8914 0.716 1.232l-2.219 1.72c-0.1251-1.779-1.54-3.225-3.35-3.225-1.891 0-3.441 1.542-3.441 3.433v6.724c-3.902e-4 -0.0156 3.8e-6 -0.026 0 0.02942v5.791c0 1.893 1.548 3.441 3.441 3.441 1.699 0 2.96-1.32 3.233-2.944l3.545 2.366c1.574 1.049 3.718 0.6164 4.767-0.9573 1.049-1.574 0.6242-3.718-0.9494-4.767l-4.716-3.137 4.916-3.825c1.293-1.006 1.683-2.776 0.9828-4.217 0.2337-0.2976 0.3747-0.6704 0.3747-1.075 0-0.9606-0.7891-1.75-1.75-1.75-0.6534 1e-7 -1.228 0.3646-1.528 0.9004-0.01712-6.411e-4 -0.03382-0.00323-0.051-0.00391-0.08719-1.045-0.87-1.942-1.936-1.942-0.4834 4.6e-6 -0.9293 0.1707-1.281 0.4551-0.04123-0.1579-0.1067-0.3062-0.1922-0.4414 0.4963-0.1534 0.8651-0.622 0.8651-1.163 0-0.6626-0.5516-1.208-1.214-1.208-0.3405 0-0.6503 0.1447-0.8709 0.3747-0.1427-0.3849-0.5149-0.665-0.9455-0.665zm0 0.9357c0.03545 0 0.07258 0.03713 0.07258 0.07258 0 0.03545-0.03712 0.07454-0.07258 0.07454-0.03545 0-0.08239-0.03909-0.08239-0.07454 0-0.03545 0.04694-0.07258 0.08239-0.07258zm1.816 0.4374c0.076 0 0.1255 0.04954 0.1255 0.1255 0 0.076-0.04954 0.1255-0.1255 0.1255-0.076 0-0.1275-0.04954-0.1275-0.1255 0-0.076 0.05151-0.1255 0.1275-0.1255zm-0.9788 1.638c0.2727 1e-7 0.4806 0.2098 0.4806 0.4826 0 0.2727-0.2078 0.4806-0.4806 0.4806-0.2727 0-0.4826-0.2078-0.4826-0.4806 0-0.2727 0.2098-0.4826 0.4826-0.4826zm2.801 0.7258c0.5365-1.55e-5 0.9573 0.4208 0.9573 0.9573 0 0.0449-0.0071 0.09728-0.0079 0.1922-7.64e-4 0.09495 0.01177 0.2592 0.104 0.4159 0.09067 0.1538 0.2755 0.2826 0.4159 0.3256 0.1403 0.04299 0.2351 0.03781 0.3099 0.03728 0.1106 0.8569 0.8508 1.528 1.736 1.528 0.1167 3e-7 0.2308-0.01313 0.3413-0.0353 0.3731 0.951 0.2233 2.066-0.6159 2.719l-5.508 4.284a0.5428 0.5428 0 0 0 0.02942 0.8827l5.337 3.558c1.085 0.7232 1.376 2.177 0.6532 3.262-0.7232 1.085-2.177 1.376-3.262 0.6532l-4.182-2.787a0.5428 0.5428 0 0 0-0.8376 0.4512v0.3786c0 1.306-1.051 2.358-2.358 2.358-1.306 0-2.35-1.051-2.35-2.358v-5.791c-3e-6 0.04601-8.017e-4 0.04336 0-0.02156a0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3v-6.724c0-1.304 1.046-2.35 2.35-2.35 1.304 0 2.35 1.046 2.35 2.35v0.8376a0.5428 0.5428 0 0 0 0.8749 0.4296l3.182-2.468-0.02942 0.01374s0.08749-0.05788 0.1765-0.155c0.08906-0.09708 0.2481-0.2676 0.2079-0.5924-0.01894-0.1544-0.1333-0.3798-0.2452-0.4747-0.1119-0.0949-0.1512-0.1021-0.1765-0.1197-0.2538-0.1759-0.4159-0.4705-0.4159-0.7925 0-0.5365 0.4266-0.9572 0.9631-0.9573zm3.515 1.038c0.374 0 0.6669 0.2949 0.6669 0.6689 0 0.2634-0.1462 0.4853-0.3629 0.5944-5.26e-4 2.647e-4 -0.0014-2.637e-4 -2e-3 0-0.03386 0.01188-0.0579 0.02194-0.07258 0.02744-0.03712 0.01393-0.0737 0.0257-0.1118 0.03337-0.03811 0.00763-0.07742 0.01177-0.1177 0.01177-0.04676 0-0.09349-0.00497-0.1373-0.01374-0.04382-0.00873-0.0855-0.02255-0.1255-0.03923-0.04004-0.01666-0.07833-0.03705-0.1138-0.06081-0.03545-0.02376-0.06807-0.05041-0.09807-0.08042-0.03003-0.03003-0.05667-0.06264-0.08042-0.09808-0.02376-0.03545-0.04415-0.07373-0.06081-0.1138-0.01667-0.04004-0.03048-0.08173-0.03923-0.1255-0.0087-0.04381-0.01177-0.0886-0.01177-0.1354 1e-5 -0.01184 0.0038-0.04005 0.0059-0.0922 2.62e-4 -0.00664-1.63e-4 -0.00398 0-0.01177 0.04808-0.3236 0.3221-0.5649 0.6611-0.5649z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.5 KiB |
|
@ -35,8 +35,8 @@ import * as ui from './ui.js'
|
|||
import * as utils from './utils.js'
|
||||
|
||||
// Import CSS files
|
||||
import './styles.scss'
|
||||
import './kx-styles.scss'
|
||||
import './styles.css'
|
||||
import './kx-styles.css'
|
||||
|
||||
// Add functions to window
|
||||
window.showQR = utils.showQR
|
||||
|
|
|
@ -45,12 +45,11 @@ export class Claim extends HTMLElement {
|
|||
}
|
||||
|
||||
async verify() {
|
||||
const claim = doipjs.Claim.fromJSON(JSON.parse(this.getAttribute('data-claim')));
|
||||
const claim = new doipjs.Claim(JSON.parse(this.getAttribute('data-claim')));
|
||||
await claim.verify({
|
||||
proxy: {
|
||||
policy: 'adaptive',
|
||||
hostname: 'PLACEHOLDER__PROXY_HOSTNAME',
|
||||
scheme: 'PLACEHOLDER__PROXY_SCHEME'
|
||||
hostname: 'PLACEHOLDER__PROXY_HOSTNAME'
|
||||
}
|
||||
});
|
||||
this.setAttribute('data-claim', JSON.stringify(claim));
|
||||
|
@ -58,31 +57,36 @@ export class Claim extends HTMLElement {
|
|||
|
||||
updateContent(value) {
|
||||
const root = this;
|
||||
const claimJson = JSON.parse(value);
|
||||
const claim = doipjs.Claim.fromJSON(claimJson);
|
||||
const claim = new doipjs.Claim(JSON.parse(value));
|
||||
|
||||
root.querySelector('.info .title').innerText = claimJson.display.profileName;
|
||||
root.querySelector('.info .subtitle').innerText = claimJson.display.serviceProviderName ??
|
||||
(claim.status < 300 ? '???' : '---');
|
||||
root.querySelector('.info img').setAttribute('src',
|
||||
`https://design.keyoxide.org/brands/service-providers/${claimJson.display.serviceProviderId
|
||||
? claimJson.display.serviceProviderId : '_'}/icon.svg`);
|
||||
switch (claim.matches[0].serviceprovider.name) {
|
||||
case 'dns':
|
||||
case 'xmpp':
|
||||
case 'irc':
|
||||
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name.toUpperCase();
|
||||
break;
|
||||
|
||||
default:
|
||||
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name;
|
||||
break;
|
||||
}
|
||||
root.querySelector('.info .title').innerText = claim.matches[0].profile.display;
|
||||
|
||||
try {
|
||||
if (claim.status >= 200) {
|
||||
root.setAttribute('data-status', claim.status < 300 ? 'success' : 'failed');
|
||||
if (claim.status === 'verified') {
|
||||
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.verification.result ? 'success' : 'failed');
|
||||
} else {
|
||||
root.setAttribute('data-status', 'running');
|
||||
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
|
||||
}
|
||||
} catch (error) {
|
||||
root.setAttribute('data-status', 'failed');
|
||||
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed');
|
||||
}
|
||||
|
||||
const elContent = root.querySelector('.content');
|
||||
elContent.innerHTML = ``;
|
||||
|
||||
// Handle failed ambiguous claim
|
||||
if (claim.status >= 300 && claim.isAmbiguous()) {
|
||||
if (claim.status === 'verified' && !claim.verification.result && claim.isAmbiguous()) {
|
||||
root.querySelector('.info .subtitle').innerText = '---';
|
||||
|
||||
const subsection_alert = elContent.appendChild(document.createElement('div'));
|
||||
|
@ -108,15 +112,15 @@ export class Claim extends HTMLElement {
|
|||
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
|
||||
|
||||
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||
if (claimJson.display.profileUrl) {
|
||||
profile_link.innerHTML = `Profile link: <a rel="me" href="${claimJson.display.profileUrl}" aria-label="link to profile">${claimJson.display.profileUrl}</a>`;
|
||||
if (claim.matches[0].profile.uri) {
|
||||
profile_link.innerHTML = `Profile link: <a rel="me" href="${claim.matches[0].profile.uri}" aria-label="link to profile">${claim.matches[0].profile.uri}</a>`;
|
||||
} else {
|
||||
profile_link.innerHTML = `Profile link: not accessible from browser`;
|
||||
}
|
||||
|
||||
const proof_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||
if (claimJson.display.proofUrl) {
|
||||
proof_link.innerHTML = `Proof link: <a href="${claimJson.display.proofUrl}" aria-label="link to profile">${claimJson.display.proofUrl}</a>`;
|
||||
if (claim.matches[0].proof.uri) {
|
||||
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.uri}" aria-label="link to profile">${claim.matches[0].proof.uri}</a>`;
|
||||
} else {
|
||||
proof_link.innerHTML = `Proof link: not accessible from browser`;
|
||||
}
|
||||
|
@ -151,7 +155,7 @@ export class Claim extends HTMLElement {
|
|||
const subsection_status_text = subsection_status.appendChild(document.createElement('div'));
|
||||
|
||||
const verification = subsection_status_text.appendChild(document.createElement('p'));
|
||||
if (claim.status >= 200) {
|
||||
if (claim.status === 'verified') {
|
||||
verification.innerHTML = `Claim verification has completed.`;
|
||||
subsection_status_icon.setAttribute('src', '/static/img/check-decagram.svg');
|
||||
subsection_status_icon.setAttribute('alt', '');
|
||||
|
@ -173,10 +177,10 @@ export class Claim extends HTMLElement {
|
|||
const subsection_result_text = subsection_result.appendChild(document.createElement('div'));
|
||||
|
||||
const result = subsection_result_text.appendChild(document.createElement('p'));
|
||||
result.innerHTML = `The claim <strong>${claim.status >= 200 && claim.status < 300 ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
|
||||
result.innerHTML = `The claim <strong>${claim.verification.result ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
|
||||
|
||||
// Additional info
|
||||
if (claim.status === 201) {
|
||||
if (claim.verification.proof.viaProxy) {
|
||||
elContent.appendChild(document.createElement('hr'));
|
||||
|
||||
const subsection_info = elContent.appendChild(document.createElement('div'));
|
||||
|
@ -188,7 +192,7 @@ export class Claim extends HTMLElement {
|
|||
const subsection_info_text = subsection_info.appendChild(document.createElement('div'));
|
||||
|
||||
const result_proxyUsed = subsection_info_text.appendChild(document.createElement('p'));
|
||||
result_proxyUsed.innerHTML = `A proxy was used to fetch the proof: <a href="PLACEHOLDER__PROXY_SCHEME://PLACEHOLDER__PROXY_HOSTNAME" aria-label="Link to proxy server">PLACEHOLDER__PROXY_HOSTNAME</a>`;
|
||||
result_proxyUsed.innerHTML = `A proxy was used to fetch the proof: <a href="https://PLACEHOLDER__PROXY_HOSTNAME" aria-label="Link to proxy server">PLACEHOLDER__PROXY_HOSTNAME</a>`;
|
||||
}
|
||||
|
||||
// TODO Display errors
|
||||
|
@ -213,4 +217,4 @@ export class Claim extends HTMLElement {
|
|||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ export class Key extends HTMLElement {
|
|||
const root = this;
|
||||
const data = JSON.parse(value);
|
||||
|
||||
root.querySelector('.info .subtitle').innerText = `${data.keyType} / ${data.fetch.method}`;
|
||||
root.querySelector('.info .subtitle').innerText = data.key.fetchMethod;
|
||||
root.querySelector('.info .title').innerText = data.fingerprint;
|
||||
|
||||
const elContent = root.querySelector('.content');
|
||||
|
@ -62,24 +62,22 @@ export class Key extends HTMLElement {
|
|||
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
|
||||
|
||||
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||
profile_link.innerHTML = `Key link: <a class="u-key" rel="pgpkey" href="${data.fetch.resolvedUrl}" aria-label="Link to cryptographic key">${data.fetch.resolvedUrl}</a>`;
|
||||
profile_link.innerHTML = `Key link: <a class="u-key" rel="pgpkey" href="${data.key.uri}" aria-label="Link to cryptographic key">${data.key.uri}</a>`;
|
||||
|
||||
if (data.keyType === 'openpgp') {
|
||||
elContent.appendChild(document.createElement('hr'));
|
||||
|
||||
// QR Code
|
||||
const subsection_qr = elContent.appendChild(document.createElement('div'));
|
||||
subsection_qr.setAttribute('class', 'subsection');
|
||||
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
|
||||
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.svg');
|
||||
subsection_qr_icon.setAttribute('alt', '');
|
||||
subsection_qr_icon.setAttribute('aria-hidden', 'true');
|
||||
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
|
||||
|
||||
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
|
||||
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
|
||||
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
|
||||
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
|
||||
}
|
||||
elContent.appendChild(document.createElement('hr'));
|
||||
|
||||
// QR Code
|
||||
const subsection_qr = elContent.appendChild(document.createElement('div'));
|
||||
subsection_qr.setAttribute('class', 'subsection');
|
||||
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
|
||||
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.svg');
|
||||
subsection_qr_icon.setAttribute('alt', '');
|
||||
subsection_qr_icon.setAttribute('aria-hidden', 'true');
|
||||
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
|
||||
|
||||
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
|
||||
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
|
||||
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
|
||||
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
|
||||
}
|
||||
}
|
264
static-src/kx-styles.css
Normal file
264
static-src/kx-styles.css
Normal file
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
kx-claim {
|
||||
display: block;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* KX-ITEM */
|
||||
.kx-item details {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.kx-item details p {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.kx-item details a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
.kx-item details hr {
|
||||
border: none;
|
||||
border-top: 2px solid var(--claim-background-color);
|
||||
}
|
||||
.kx-item details .content {
|
||||
padding: 12px;
|
||||
border: solid 3px var(--claim-background-color);
|
||||
border-top: 0px;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
}
|
||||
.kx-item details summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--claim-background-color);
|
||||
border: solid 3px var(--claim-background-color);
|
||||
border-radius: 8px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.kx-item details summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.kx-item details summary:hover, summary:focus {
|
||||
border-color: var(--claim-border-accent-color);
|
||||
}
|
||||
details[open] summary {
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
}
|
||||
.kx-item details summary .info {
|
||||
flex: 1;
|
||||
}
|
||||
.kx-item details summary .info .title {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.kx-item details summary .claim__description p {
|
||||
font-size: 1.4rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
.kx-item details summary .claim__links p, p.subtle-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1rem;
|
||||
color: var(--link-subtle-color);
|
||||
}
|
||||
.kx-item details summary .claim__links a, summary .claim__links span, p.subtle-links a {
|
||||
font-size: 1rem;
|
||||
margin: 0 10px 0 0;
|
||||
color: var(--link-subtle-color);
|
||||
}
|
||||
.kx-item details summary .subtitle {
|
||||
color: var(--claim-title-text-color);
|
||||
}
|
||||
.kx-item details summary .verificationStatus {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 100%;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
user-select: none;
|
||||
}
|
||||
.kx-item details summary .verificationStatus::after {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.kx-item details summary .verificationStatus .inProgress {
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.kx-item details summary .verificationStatus[data-value="success"] {
|
||||
content: "v";
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
.kx-item details summary .verificationStatus[data-value="success"]::after {
|
||||
content: "✔";
|
||||
}
|
||||
.kx-item details summary .verificationStatus[data-value="failed"] {
|
||||
background-color: var(--failure-color);
|
||||
}
|
||||
.kx-item details summary .verificationStatus[data-value="failed"]::after {
|
||||
content: "✕";
|
||||
}
|
||||
.kx-item details summary .verificationStatus[data-value="running"] .inProgress {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.kx-item details .subsection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.kx-item details .subsection > img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.kx-item details .subsection > img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
.kx-item details .inProgress {
|
||||
font-size: 10px;
|
||||
margin: 50px auto;
|
||||
text-indent: -9999em;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--loader-color);
|
||||
background: -moz-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
|
||||
background: -webkit-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
|
||||
background: -o-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
|
||||
background: -ms-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
|
||||
background: linear-gradient(to right, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
|
||||
position: relative;
|
||||
-webkit-animation: load3 1.4s infinite linear;
|
||||
animation: load3 1.4s infinite linear;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
.kx-item details .inProgress:before {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background: var(--loader-color);
|
||||
border-radius: 100% 0 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: '';
|
||||
}
|
||||
.kx-item details .inProgress:after {
|
||||
background: var(--claim-background-color);
|
||||
width: 65%;
|
||||
height: 65%;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
.kx-item details button {
|
||||
padding: 0.4rem 0.8rem;
|
||||
margin-right: 8px;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
border: solid 2px var(--button-border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.kx-item details button:hover {
|
||||
background-color: var(--button-hover-background-color);
|
||||
border-color: var(--button-hover-border-color);
|
||||
color: var(--button-hover-text-color);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.kx-item details summary .claim__description p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.kx-item details summary .claim__links a, p.subtle-links a {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
summary .claim__description p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.kx-item details summary .verificationStatus {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.kx-item details .inProgress {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes load3 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes load3 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -1,284 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
kx-claim {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.kx-item {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
margin-left: -4px;
|
||||
|
||||
details {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
z-index: 0;
|
||||
|
||||
&[open] {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 8px 0;
|
||||
border: none;
|
||||
border-top: 2px solid var(--header-background-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 0.9em;
|
||||
padding: 12px;
|
||||
background-color: var(--background-color);
|
||||
border-top: 0px;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
}
|
||||
|
||||
&[open] summary {
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
background-color: var(--header-background-color) !important;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--header-background-color);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info .title {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.info img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
transform: translateY(3px);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
.claim__links {
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 1em;
|
||||
color: var(--link-color-subtle);
|
||||
}
|
||||
|
||||
.a,
|
||||
span {
|
||||
font-size: 1em;
|
||||
margin: 0 10px 0 0;
|
||||
color: var(--link-color-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle-wrapper {
|
||||
color: var(--text-color-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
.subsection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.subsection>img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.subsection>img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.verificationStatus {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
color: #fff;
|
||||
font-size: 1.4em;
|
||||
user-select: none;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.inProgress,
|
||||
.success,
|
||||
.failure {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.inProgress {
|
||||
color: var(--loader-color);
|
||||
|
||||
svg {
|
||||
animation: 1s linear 0s infinite rot360;
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.failure {
|
||||
color: var(--failure-color);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.4rem 0.8em;
|
||||
margin-right: 8px;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
border: solid 1px var(--button-border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-background-color-hover);
|
||||
border-color: var(--button-border-color-hover);
|
||||
color: var(--button-text-color-hover);
|
||||
}
|
||||
|
||||
&[data-status="running"] {
|
||||
.title {
|
||||
color: var(--text-color-subtle) !important;
|
||||
}
|
||||
.inProgress {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="success"] {
|
||||
.title {
|
||||
color: var(--primary-color) !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
.success {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="failed"] {
|
||||
.title {
|
||||
color: var(--text-color-subtle) !important;
|
||||
}
|
||||
.failure {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status="success"] .verificationStatus>div,
|
||||
&[data-status="failed"] .verificationStatus>div {
|
||||
transition: opacity 0.4s ease !important;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rot360 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rot360 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
645
static-src/styles.css
Normal file
645
static-src/styles.css
Normal file
|
@ -0,0 +1,645 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@import '../node_modules/fork-awesome/css/fork-awesome.css';
|
||||
@import '../node_modules/dialog-polyfill/dist/dialog-polyfill.css';
|
||||
|
||||
:root {
|
||||
--white: hsl(0, 0%, 100%);
|
||||
--black: hsl(0, 0%, 0%);
|
||||
--grey-100: hsl(0, 0%, 90%);
|
||||
--grey-200: hsl(0, 0%, 80%);
|
||||
--grey-300: hsl(0, 0%, 70%);
|
||||
--grey-400: hsl(0, 0%, 60%);
|
||||
--grey-500: hsl(0, 0%, 50%);
|
||||
--grey-600: hsl(0, 0%, 40%);
|
||||
--grey-700: hsl(0, 0%, 30%);
|
||||
--grey-900: hsl(0, 0%, 10%);
|
||||
--green-300: hsl(110, 45%, 70%);
|
||||
--green-400: hsl(110, 45%, 60%);
|
||||
--green-600: hsl(110, 45%, 40%);
|
||||
--red-400: hsl(10, 60%, 60%);
|
||||
--blue-500: rgb(67, 176, 234);
|
||||
--blue-700: hsl(201, 90%, 30%);
|
||||
--purple-50: rgb(249, 248, 251);
|
||||
--purple-100: rgb(238, 236, 248);
|
||||
--purple-200: hsl(250, 48%, 90%);
|
||||
--purple-300: hsl(250, 48%, 85%);
|
||||
--purple-400: hsl(250, 48%, 70%);
|
||||
--purple-500: hsl(250, 48%, 65%);
|
||||
--purple-600: hsl(250, 48%, 60%);
|
||||
--purple-700: hsl(250, 48%, 55%);
|
||||
--purple-900: hsl(250, 38%, 45%);
|
||||
--yellow-100: hsl(56, 100%, 95%);
|
||||
--yellow-200: hsl(56, 100%, 90%);
|
||||
--yellow-500: hsl(56, 100%, 65%);
|
||||
|
||||
--loader-color: var(--purple-400);
|
||||
--success-color: var(--green-600);
|
||||
--failure-color: var(--red-400);
|
||||
|
||||
--text-color: var(--grey-900);
|
||||
--h1-color: var(--purple-700);
|
||||
--h2-color: var(--purple-700);
|
||||
--h2-small-color: var(--purple-600);
|
||||
--h3-color: var(--grey-700);
|
||||
--h3-small-color: var(--purple-400);
|
||||
--h4-color: var(--grey-600);
|
||||
--h4-small-color: var(--purple-400);
|
||||
--link-color: var(--blue-700);
|
||||
--link-subtle-color: var(--grey-700);
|
||||
--nav-link-color: var(--purple-700);
|
||||
|
||||
--button-text-color: var(--text-color);
|
||||
--button-border-color: var(--purple-500);
|
||||
--button-background-color: var(--white);
|
||||
--button-hover-text-color: var(--white);
|
||||
--button-hover-border-color: var(--purple-500);
|
||||
--button-hover-background-color: var(--purple-500);
|
||||
|
||||
--body-background-color: var(--white);
|
||||
--footer-background-color: var(--purple-900);
|
||||
--footer-text-color: var(--purple-200);
|
||||
--card-background-color: var(--purple-50);
|
||||
--card-border-color: var(--purple-200);
|
||||
--claim-background-color: var(--purple-100);
|
||||
--claim-border-accent-color: var(--purple-400);
|
||||
--claim-title-text-color: var(--purple-700);
|
||||
--input-focus-background-color: azure;
|
||||
--focus-outline-color: lightskyblue;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-color: var(--grey-100);
|
||||
--h1-color: var(--purple-700);
|
||||
--h2-color: var(--purple-300);
|
||||
--h2-small-color: var(--purple-600);
|
||||
--h3-color: var(--grey-300);
|
||||
--h3-small-color: var(--purple-400);
|
||||
--h4-color: var(--grey-300);
|
||||
--h4-small-color: var(--purple-6400);
|
||||
--link-color: var(--blue-500);
|
||||
--link-subtle-color: var(--grey-700);
|
||||
--nav-link-color: var(--purple-100);
|
||||
|
||||
--loader-color: var(--purple-600);
|
||||
--success-color: var(--green-600);
|
||||
--failure-color: var(--red-400);
|
||||
|
||||
--button-text-color: var(--white);
|
||||
--button-border-color: var(--purple-700);
|
||||
--button-background-color: var(--purple-900);
|
||||
--button-hover-text-color: var(--white);
|
||||
--button-hover-border-color: var(--purple-700);
|
||||
--button-hover-background-color: var(--purple-700);
|
||||
|
||||
--body-background-color: #121212;
|
||||
--footer-background-color: #191720;
|
||||
--footer-text-color: var(--purple-200);
|
||||
--card-background-color: #191720;
|
||||
--card-border-color: #26203a;
|
||||
--claim-background-color: #26203a;
|
||||
--claim-border-accent-color: var(--purple-400);
|
||||
--claim-title-text-color: var(--purple-300);
|
||||
--input-focus-background-color: azure;
|
||||
--focus-outline-color: lightskyblue;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--focus-outline-color);
|
||||
}
|
||||
input:focus, textarea:focus {
|
||||
background: var(--input-focus-background-color);
|
||||
}
|
||||
input[type="radio"]:focus + label {
|
||||
box-shadow: 0 0 0 3px var(--focus-outline-color);
|
||||
background: var(--input-focus-background-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 1.6rem 0 0;
|
||||
line-height: 1.4rem;
|
||||
font-family: sans-serif;
|
||||
color: var(--text-color);
|
||||
background-color: var(--body-background-color);
|
||||
}
|
||||
|
||||
/* HELPERS */
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.full-width {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
}
|
||||
.half-width {
|
||||
display: block;
|
||||
width: 50% !important;
|
||||
}
|
||||
.select-all {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
/* LAYOUT */
|
||||
header {
|
||||
margin: 0 1.6rem 1.6rem;
|
||||
}
|
||||
header nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
header nav a.logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
font-size: 1.6rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
color: var(--nav-link-color);
|
||||
}
|
||||
header nav a.logo img {
|
||||
width: 100%;
|
||||
}
|
||||
nav a.text {
|
||||
/* font-size: 0.9em; */
|
||||
margin: 0;
|
||||
padding: 0.5em 1em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
color: var(--nav-link-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
nav a.text:hover, nav a.text:active {
|
||||
color: #fff;
|
||||
background-color: var(--purple-500);
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
margin: 0 1.6rem;
|
||||
}
|
||||
footer {
|
||||
margin: 4.8rem 0 0;
|
||||
padding: 0 1.6rem 1.6rem;
|
||||
background-color: var(--footer-background-color);
|
||||
color: var(--footer-background-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
section.profile p, .demo p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.demo {
|
||||
margin: 4.8rem auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0 0 1.6rem;
|
||||
padding: 0 1.2rem;
|
||||
background-color: #fff;
|
||||
background-color: var(--card-background-color);
|
||||
border: 2px solid var(--card-border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.card.card--transparent {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.card--profileHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.card--profileHeader p, .card--profileHeader small {
|
||||
margin: 0;
|
||||
}
|
||||
.card--small-profile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
.card--small-profile-dummy {
|
||||
opacity: 0.5;
|
||||
border: 0;
|
||||
}
|
||||
.card--small-profile .name {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
.card--small-profile p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.card--small-profile p span.fingerprint {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
#profileName {
|
||||
font-size: 1.6rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
#profileURLFingerprint {
|
||||
font-size: 1rem;
|
||||
margin: 0 0 1.2rem;
|
||||
}
|
||||
|
||||
.hcards {
|
||||
display: grid;
|
||||
grid-gap: 1.2rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
.hcards .card {
|
||||
margin: 0;
|
||||
}
|
||||
.hcards.hcards--max-2 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
|
||||
}
|
||||
.hcards--col-1-2, .hcards--col-2-1 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
|
||||
}
|
||||
.hcards--col-1-2 .card, .hcards--col-2-1 .card {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
@media screen and (min-width: 1024px) {
|
||||
.hcards--max-3 {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
.hcards--max-2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.hcards--col-1-2, .hcards--col-2-1 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 720px) {
|
||||
.hcards--col-2-1 .card:nth-of-type(1) {
|
||||
grid-column: 1 / -2;
|
||||
}
|
||||
.hcards--col-2-1 .card:nth-of-type(2) {
|
||||
grid-column: -2 / -1;
|
||||
}
|
||||
.hcards--col-1-2 .card:nth-of-type(1) {
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
.hcards--col-1-2 .card:nth-of-type(2) {
|
||||
grid-column: 2 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
padding: calc(0.8rem - 2px) 0.8rem;
|
||||
background-color: var(--yellow-200);
|
||||
border: solid 2px var(--yellow-500);
|
||||
}
|
||||
.warning p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
.warning p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#profileAvatar {
|
||||
display: inline-block;
|
||||
min-width: 96px;
|
||||
max-width: 128px;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* TYPOGRAPHY */
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
margin: 3.2rem 0 1.6rem;
|
||||
font-weight: normal;
|
||||
color: var(--h1-color);
|
||||
cursor: default;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.4em;
|
||||
margin: 3.2rem 0 1.6rem;
|
||||
font-weight: normal;
|
||||
color: var(--h2-color);
|
||||
cursor: default;
|
||||
}
|
||||
h2 small {
|
||||
margin-left: 0.8rem;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--h2-small-color);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
h3 {
|
||||
margin: 1.6rem 0;
|
||||
font-size: 1.3em;
|
||||
line-height: 1.6rem;
|
||||
color: var(--h3-color);
|
||||
font-weight: normal;
|
||||
/* text-align: center; */
|
||||
cursor: default;
|
||||
}
|
||||
h3 small {
|
||||
margin-left: 0.8rem;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--h3-small-color);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
h4 {
|
||||
margin: 1.6rem 0;
|
||||
font-size: 1em;
|
||||
line-height: 1.6rem;
|
||||
color: var(--h4-color);
|
||||
/* color: var(--purple-700); */
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
h4 small {
|
||||
margin-left: 0.8rem;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--h4-small-color);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
p {
|
||||
margin: 1.6rem 0;
|
||||
}
|
||||
p.warning {
|
||||
padding: 8px;
|
||||
background-color: #fffadc;
|
||||
border: solid 1px #ffeea8;
|
||||
}
|
||||
a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
list-style: '- ';
|
||||
}
|
||||
main h1:first-of-type {
|
||||
margin-top: 1.6rem;
|
||||
}
|
||||
footer h1 {
|
||||
margin-bottom: 0.8rem;
|
||||
color: var(--purple-200);
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
footer a {
|
||||
display: inline-block;
|
||||
color: var(--purple-100);
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
background-color: var(--purple-100);
|
||||
border: 1px solid var(--purple-500);
|
||||
}
|
||||
pre {
|
||||
padding: 8px 12px;
|
||||
background-color: var(--purple-100);
|
||||
border: 1px solid var(--purple-500);
|
||||
overflow-x: auto;
|
||||
line-height: 1.2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
background-color: 0px;
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
#search {
|
||||
margin-top: 96px;
|
||||
margin-bottom: 128px;
|
||||
}
|
||||
#qr {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
max-width: 256px !important;
|
||||
height: auto !important;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
/* FORM ELEMENTS */
|
||||
.form-wrapper {
|
||||
align-items: center;
|
||||
padding-top: 1.4rem;
|
||||
padding-bottom: 1.6rem;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
.form-wrapper *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.form-wrapper form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
.form-wrapper h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
form input[type="text"], form input[type="search"] {
|
||||
margin: 8px 0;
|
||||
padding: 4px;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
form textarea {
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
margin: 8px 0;
|
||||
resize: vertical;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.radio-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.radio-wrapper input[type="radio"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
.radio-wrapper input[type="radio"] + label {
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
background-color: #fff;
|
||||
border: solid var(--purple-400);
|
||||
border-width: 2px 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.radio-wrapper input[type="radio"]:first-of-type + label {
|
||||
border-radius: 4px 0 0 4px;
|
||||
border-left-width: 2px;
|
||||
}
|
||||
.radio-wrapper input[type="radio"]:last-of-type + label {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 2px;
|
||||
}
|
||||
.radio-wrapper input[type="radio"]:focus + label {
|
||||
z-index: 1;
|
||||
}
|
||||
.radio-wrapper input[type="radio"] + label:hover {
|
||||
background-color: var(--purple-100);
|
||||
border-color: var(--purple-500);
|
||||
}
|
||||
.radio-wrapper input[type="radio"]:checked + label {
|
||||
color: #fff;
|
||||
background-color: var(--purple-600);
|
||||
border-color: var(--purple-600);
|
||||
}
|
||||
|
||||
input[type="button"], input[type="submit"], button, a.button {
|
||||
display: inline-block;
|
||||
min-height: 36px;
|
||||
margin: 8px 0;
|
||||
padding: 4px 8px;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
border: solid 2px var(--button-border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="button"]:focus, input[type="submit"]:focus, button:focus, a.button:focus {
|
||||
background-color: var(--input-focus-background-color);
|
||||
}
|
||||
input[type="button"]:hover, input[type="submit"]:hover, button:hover, a.button:hover {
|
||||
background-color: var(--button-hover-background-color);
|
||||
border-color: var(--button-hover-border-color);
|
||||
color: var(--button-hover-text-color);
|
||||
}
|
||||
a.button i {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
a.button--donate {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: 12px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.95rem;
|
||||
border: 0;
|
||||
}
|
||||
a.button--donate svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
a.button--donate.button--liberapay {
|
||||
color: #333;
|
||||
background-color: #ffee16;
|
||||
}
|
||||
a.button--donate.button--liberapay svg {
|
||||
fill: #333;
|
||||
}
|
||||
a.button--donate.button--liberapay:hover {
|
||||
color: #333;
|
||||
background-color: #fff463;
|
||||
}
|
||||
a.button--donate.button--kofi {
|
||||
color: #333;
|
||||
background-color: #1ac0ff;
|
||||
}
|
||||
a.button--donate.button--kofi svg {
|
||||
fill: #333;
|
||||
}
|
||||
a.button--donate.button--kofi:hover {
|
||||
color: #333;
|
||||
background-color: #66d4ff;
|
||||
}
|
||||
button.inline {
|
||||
min-height: auto;
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* DIALOGS */
|
||||
dialog {
|
||||
width: 100% !important;
|
||||
max-width: 800px !important;
|
||||
padding: 0 !important;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
dialog > div {
|
||||
padding: 1em;
|
||||
}
|
||||
dialog form[method="Dialog"] {
|
||||
margin: 1em 0 0 !important;
|
||||
}
|
||||
dialog form[method="Dialog"] input {
|
||||
width: auto;
|
||||
}
|
||||
dialog p {
|
||||
font-size: 1rem !important;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
dialog p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
.form-wrapper {
|
||||
align-items: center;
|
||||
padding-top: 1.4rem;
|
||||
padding-bottom: 1.6rem;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.form-wrapper *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-wrapper form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-wrapper h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
form input[type="text"],
|
||||
form input[type="search"] {
|
||||
margin: 8px 0;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
form textarea {
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
margin: 8px 0;
|
||||
resize: vertical;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--input-border-color);
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
color: var(--input-text-color);
|
||||
background-color: var(--input-background-color);
|
||||
border: solid 1px var(--input-border-color);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--input-background-color-hover);
|
||||
border-color: var(--input-border-color-hover);
|
||||
color: var(--input-text-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="button"],
|
||||
input[type="submit"],
|
||||
button,
|
||||
a.button {
|
||||
display: inline-block;
|
||||
margin: 8px 0;
|
||||
padding: 4px 8px;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
border: solid 1px var(--button-border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-background-color-hover);
|
||||
border-color: var(--button-border-color-hover);
|
||||
color: var(--button-text-color-hover);
|
||||
}
|
||||
|
||||
&.themed {
|
||||
padding: 8px 12px;
|
||||
color: var(--text-color-inverse);
|
||||
background-color: var(--primary-color);
|
||||
border: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color-inverse);
|
||||
background-color: var(--primary-color-subtle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
button.inline {
|
||||
min-height: auto;
|
||||
margin: 0;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
#search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 50vh;
|
||||
gap: 48px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
||||
& > svg {
|
||||
width: 96px;
|
||||
fill: var(--primary-color);
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
max-width: 30em;
|
||||
|
||||
input {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
border-left: 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
hr {
|
||||
margin: 3em 0;
|
||||
color: var(--line-color-subtle);
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0 8px;
|
||||
padding: 1.6rem 0 0;
|
||||
line-height: 1.4rem;
|
||||
font-family: sans-serif;
|
||||
color: var(--text-color);
|
||||
background-color: var(--body-background-color);
|
||||
}
|
||||
|
||||
header {
|
||||
margin: 0 0 3rem;
|
||||
|
||||
nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
a.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 1.8rem;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
gap: 8px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
|
||||
&:hover, &:active {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
footer {
|
||||
margin: 4.8rem 0 0;
|
||||
background-color: var(--footer-background-color);
|
||||
color: var(--footer-text-color);
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: var(--footer-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 0 0 32px;
|
||||
padding: 16px;
|
||||
background-color: var(--section-background-color);
|
||||
border-radius: 8px;
|
||||
|
||||
h1:first-child, h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding-top: 24px;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.profile__name {
|
||||
font-size: 1.6rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.profile__header {
|
||||
flex: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
& p, & small {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.profile__claims {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.profile__avatar {
|
||||
display: inline-block;
|
||||
min-width: 96px;
|
||||
max-width: 128px;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.persona__description {
|
||||
display: block;
|
||||
margin: 8px 0 8px;
|
||||
font-size: 0.9em;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.screenshots {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background-color: #fafafa;
|
||||
overflow-y: scroll;
|
||||
|
||||
img {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.banners {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
a {
|
||||
img {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
h1 {
|
||||
font-size: 1.6em;
|
||||
margin: 3.2rem 0 1.6rem;
|
||||
font-weight: normal;
|
||||
color: var(--h1-color);
|
||||
cursor: default;
|
||||
}
|
||||
h2 {
|
||||
margin: 1em 0 0.5em;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
color: var(--h2-color);
|
||||
cursor: default;
|
||||
|
||||
small {
|
||||
margin-left: 0.8rem;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--h2-small-color);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
margin: 1em 0 0.5em;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.6rem;
|
||||
color: var(--h3-color);
|
||||
font-weight: normal;
|
||||
cursor: default;
|
||||
|
||||
small {
|
||||
margin-left: 0.8rem;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--h3-small-color);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin: 1.6rem 0;
|
||||
font-size: 1em;
|
||||
line-height: 1.6rem;
|
||||
color: var(--h4-color);
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
|
||||
small {
|
||||
margin-left: 0.8rem;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--h4-small-color);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: var(--link-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color-hover);
|
||||
}
|
||||
}
|
||||
a.button i {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
a.button.button--donate {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-right: 12px;
|
||||
padding: 8px 16px;
|
||||
font-size: 0.95rem;
|
||||
border: 0;
|
||||
}
|
||||
a.button.button--donate svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
a.button.button--donate.button--opencollective {
|
||||
color: #fff;
|
||||
background-color: #0c2d66;
|
||||
}
|
||||
a.button.button--donate.button--opencollective svg {
|
||||
fill: #fff;
|
||||
}
|
||||
a.button.button--donate.button--opencollective:hover {
|
||||
color: #fff;
|
||||
background-color: #144aa9;
|
||||
}
|
||||
a.button.button--donate.button--liberapay {
|
||||
color: #333;
|
||||
background-color: #ffee16;
|
||||
}
|
||||
a.button.button--donate.button--liberapay svg {
|
||||
fill: #333;
|
||||
}
|
||||
a.button.button--donate.button--liberapay:hover {
|
||||
color: #333;
|
||||
background-color: #fff463;
|
||||
}
|
||||
a.button.button--donate.button--kofi {
|
||||
color: #333;
|
||||
background-color: #1ac0ff;
|
||||
}
|
||||
a.button.button--donate.button--kofi svg {
|
||||
fill: #333;
|
||||
}
|
||||
a.button.button--donate.button--kofi:hover {
|
||||
color: #333;
|
||||
background-color: #66d4ff;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
list-style: '- ';
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
:root {
|
||||
--white: hsl(0, 0%, 100%);
|
||||
--black: hsl(0, 0%, 0%);
|
||||
--grey-10: hsl(0, 0%, 99%);
|
||||
--grey-50: hsl(0, 0%, 95%);
|
||||
--grey-100: hsl(0, 0%, 90%);
|
||||
--grey-200: hsl(0, 0%, 80%);
|
||||
--grey-300: hsl(0, 0%, 70%);
|
||||
--grey-400: hsl(0, 0%, 60%);
|
||||
--grey-500: hsl(0, 0%, 50%);
|
||||
--grey-600: hsl(0, 0%, 40%);
|
||||
--grey-700: hsl(0, 0%, 30%);
|
||||
--grey-800: hsl(0, 0%, 20%);
|
||||
--grey-900: hsl(0, 0%, 10%);
|
||||
--grey-950: hsl(0, 0%, 7%);
|
||||
--green-300: hsl(110, 45%, 70%);
|
||||
--green-400: hsl(110, 45%, 60%);
|
||||
--green-600: hsl(110, 45%, 40%);
|
||||
--red-300: hsl(10, 60%, 70%);
|
||||
--red-400: hsl(10, 60%, 60%);
|
||||
--blue-500: rgb(67, 176, 234);
|
||||
--blue-700: hsl(201, 90%, 30%);
|
||||
--purple-50: rgb(249, 248, 251);
|
||||
--purple-100: rgb(238, 236, 248);
|
||||
--purple-200: hsl(250, 48%, 90%);
|
||||
--purple-300: hsl(250, 48%, 80%);
|
||||
--purple-400: hsl(250, 48%, 70%);
|
||||
--purple-500: hsl(250, 48%, 65%);
|
||||
--purple-600: hsl(250, 48%, 60%);
|
||||
--purple-700: hsl(250, 48%, 55%);
|
||||
--purple-900: hsl(250, 38%, 45%);
|
||||
--yellow-100: hsl(56, 100%, 95%);
|
||||
--yellow-200: hsl(56, 100%, 90%);
|
||||
--yellow-500: hsl(56, 100%, 65%);
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color-light: var(--purple-700);
|
||||
--primary-color-dark: var(--purple-300);
|
||||
|
||||
--primary-color-subtle-light: var(--purple-400);
|
||||
--primary-color-subtle-dark: var(--purple-500);
|
||||
|
||||
--background-color-light: #fafafa;
|
||||
--background-color-dark: #0a0a0a;
|
||||
}
|
||||
:root {
|
||||
--primary-color: var(--primary-color-light);
|
||||
--primary-color-subtle: var(--primary-color-subtle-light);
|
||||
|
||||
--body-background-color: var(--background-color-light);
|
||||
--section-background-color: var(--white);
|
||||
|
||||
--text-color: var(--grey-800);
|
||||
--text-color-subtle: var(--grey-500);
|
||||
--text-color-inverse: var(--white);
|
||||
--h1-color: var(--text-color);
|
||||
--h2-color: var(--text-color);
|
||||
--h2-small-color: var(--primary-color-subtle);
|
||||
--h3-color: var(--text-color-subtle);
|
||||
--h3-small-color: var(--primary-color-subtle);
|
||||
--h4-color: var(--text-color-subtle);
|
||||
--h4-small-color: var(--primary-color-subtle);
|
||||
|
||||
--link-color: var(--blue-700);
|
||||
--link-color-subtle: var(--text-color);
|
||||
--link-color-hover: var(--primary-color);
|
||||
|
||||
--line-color-subtle: var(--grey-200);
|
||||
|
||||
--button-text-color: var(--text-color);
|
||||
--button-text-color-hover: var(--text-color);
|
||||
--button-border-color: var(--grey-300);
|
||||
--button-border-color-hover: var(--grey-300);
|
||||
--button-background-color: var(--grey-100);
|
||||
--button-background-color-hover: var(--grey-200);
|
||||
|
||||
--input-text-color: var(--text-color);
|
||||
--input-text-color-hover: var(--text-color);
|
||||
--input-border-color: var(--grey-300);
|
||||
--input-border-color-hover: var(--grey-300);
|
||||
--input-background-color: var(--white);
|
||||
--input-background-color-hover: var(--white);
|
||||
|
||||
--footer-text-color: var(--text-color-subtle);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
|
||||
--primary-color: var(--primary-color-dark);
|
||||
--primary-color-subtle: var(--primary-color-subtle-dark);
|
||||
|
||||
--body-background-color: var(--background-color-dark);
|
||||
--section-background-color: var(--grey-900);
|
||||
|
||||
--text-color: var(--grey-50);
|
||||
--text-color-subtle: var(--grey-300);
|
||||
--text-color-inverse: var(--grey-800);
|
||||
--h1-color: var(--text-color);
|
||||
--h2-color: var(--text-color);
|
||||
--h2-small-color: var(--primary-color-subtle);
|
||||
--h3-color: var(--text-color-subtle);
|
||||
--h3-small-color: var(--primary-color-subtle);
|
||||
--h4-color: var(--text-color-subtle);
|
||||
--h4-small-color: var(--primary-color-subtle);
|
||||
|
||||
--link-color: var(--blue-500);
|
||||
--link-color-subtle: var(--text-color);
|
||||
--link-color-hover: var(--primary-color);
|
||||
|
||||
--line-color-subtle: var(--grey-700);
|
||||
|
||||
--button-text-color: var(--text-color);
|
||||
--button-text-color-hover: var(--text-color);
|
||||
--button-border-color: var(--grey-700);
|
||||
--button-border-color-hover: var(--grey-700);
|
||||
--button-background-color: var(--grey-700);
|
||||
--button-background-color-hover: var(--grey-600);
|
||||
|
||||
--input-text-color: var(--text-color);
|
||||
--input-text-color-hover: var(--text-color);
|
||||
--input-border-color: var(--grey-700);
|
||||
--input-border-color-hover: var(--grey-700);
|
||||
--input-background-color: var(--grey-800);
|
||||
--input-background-color-hover: var(--grey-800);
|
||||
|
||||
--footer-text-color: var(--text-color-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
kx-claim, kx-key {
|
||||
--loader-color: var(--grey-400);
|
||||
--success-color: var(--green-600);
|
||||
--failure-color: var(--red-400);
|
||||
|
||||
--background-color: var(--grey-10);
|
||||
--header-background-color: var(--grey-50);
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
--loader-color: var(--grey-400);
|
||||
--success-color: var(--green-400);
|
||||
--failure-color: var(--red-300);
|
||||
|
||||
--background-color: var(--grey-800);
|
||||
--header-background-color: var(--grey-700);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ You should also get your employer (if you work as a programmer) or school,
|
|||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import dialogPolyfill from 'dialog-polyfill'
|
||||
import QRCode from 'qrcode'
|
||||
import * as openpgp from 'openpgp'
|
||||
import * as utils from './utils.js'
|
||||
|
@ -50,6 +51,17 @@ const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-
|
|||
|
||||
// Initialize UI elements and event listeners
|
||||
export function init() {
|
||||
// Register modals
|
||||
document.querySelectorAll('dialog').forEach(function(d) {
|
||||
dialogPolyfill.registerDialog(d);
|
||||
d.addEventListener('click', function(ev) {
|
||||
if (ev && ev.target != d) {
|
||||
return;
|
||||
}
|
||||
d.close();
|
||||
});
|
||||
});
|
||||
|
||||
// Run context-dependent scripts
|
||||
if (elFormEncrypt) {
|
||||
runEncryptionForm()
|
||||
|
@ -337,8 +349,8 @@ const runArgon2GenerationUtility = () => {
|
|||
elFeedback.innerHTML = "";
|
||||
} else {
|
||||
let feedbackContent = "";
|
||||
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
||||
}
|
||||
if (!(elInput.value === elInput.value.toLowerCase())) {
|
||||
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
|
||||
|
@ -379,7 +391,7 @@ window.kx__fixArgon2Input = () => {
|
|||
const elInput = document.querySelector('#form-util-argon2-generate .input');
|
||||
elInput.value = elInput.value.toLowerCase();
|
||||
|
||||
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
elInput.value = `openpgp4fpr:${elInput.value}`;
|
||||
}
|
||||
|
||||
|
@ -402,8 +414,8 @@ const runBcryptGenerationUtility = () => {
|
|||
elFeedback.innerHTML = "";
|
||||
} else {
|
||||
let feedbackContent = "";
|
||||
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
||||
}
|
||||
if (!(elInput.value === elInput.value.toLowerCase())) {
|
||||
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
|
||||
|
@ -444,7 +456,7 @@ window.kx__fixBcryptInput = () => {
|
|||
const elInput = document.querySelector('#form-util-bcrypt-generate .input');
|
||||
elInput.value = elInput.value.toLowerCase();
|
||||
|
||||
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
|
||||
elInput.value = `openpgp4fpr:${elInput.value}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,20 +46,19 @@ export async function computeWKDLocalPart(localPart) {
|
|||
// Generate Keyoxide profile URL
|
||||
export async function generateProfileURL(data) {
|
||||
let hostname = data.hostname || window.location.hostname;
|
||||
let scheme = data.scheme || window.location.protocol.slice(0,-1);
|
||||
|
||||
if (data.input == "") {
|
||||
return "Waiting for input…";
|
||||
}
|
||||
switch (data.source) {
|
||||
case "wkd":
|
||||
return `${scheme}://${hostname}/${data.input}`;
|
||||
return `https://${hostname}/${data.input}`;
|
||||
break;
|
||||
case "hkp":
|
||||
if (/.*@.*\..*/.test(data.input)) {
|
||||
return `${scheme}://${hostname}/hkp/${data.input}`;
|
||||
return `https://${hostname}/hkp/${data.input}`;
|
||||
} else {
|
||||
return `${scheme}://${hostname}/${data.input}`;
|
||||
return `https://${hostname}/${data.input}`;
|
||||
}
|
||||
break;
|
||||
case "keybase":
|
||||
|
@ -68,29 +67,40 @@ export async function generateProfileURL(data) {
|
|||
return "Incorrect Keybase public key URL.";
|
||||
}
|
||||
const match = data.input.match(re);
|
||||
return `${scheme}://${hostname}/keybase/${match[1]}/${match[2]}`;
|
||||
return `https://${hostname}/keybase/${match[1]}/${match[2]}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch OpenPGP key based on information stored in window
|
||||
export async function fetchProfileKey() {
|
||||
if (window.kx.publicKey.key && window.kx.publicKey.key instanceof openpgp.PublicKey) {
|
||||
if (window.kx.key.object && window.kx.key.object instanceof openpgp.PublicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawKeyData = await fetch(window.kx.key.url)
|
||||
let key, errorMsg
|
||||
|
||||
try {
|
||||
key = (await openpgp.readKey({
|
||||
armoredKey: window.kx.publicKey.encodedKey
|
||||
binaryKey: new Uint8Array(await rawKeyData.clone().arrayBuffer())
|
||||
}))
|
||||
} catch (error) {
|
||||
} catch(error) {
|
||||
errorMsg = error.message
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
try {
|
||||
key = (await openpgp.readKey({
|
||||
armoredKey: await rawKeyData.clone().text()
|
||||
}))
|
||||
} catch (error) {
|
||||
errorMsg = error.message
|
||||
}
|
||||
}
|
||||
|
||||
if (key) {
|
||||
window.kx.publicKey.key = key
|
||||
window.kx.key.object = key
|
||||
return
|
||||
} else {
|
||||
throw new Error(`Public key could not be fetched (${errorMsg})`)
|
||||
|
@ -230,4 +240,4 @@ export async function verifyBcryptHash(input, hash) {
|
|||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
28
template.env
28
template.env
|
@ -12,22 +12,28 @@
|
|||
# $ while read -r line; do echo -nE "$line\n" ; done < public.pem > public-oneline.pem
|
||||
#ACTIVITYPUB_PUBLIC_KEY=
|
||||
|
||||
# Domain for Keyoxide Proxy server
|
||||
# To host a Keyoxide Proxy server, refer to https://docs.keyoxide.org/self-hosting/
|
||||
# Domain for DOIP Proxy server
|
||||
# Source code for the server can be found here https://codeberg.org/keyoxide/doip-proxy
|
||||
#PROXY_HOSTNAME=
|
||||
|
||||
# Domain for Dicebear API server
|
||||
# Defaults to: api.dicebear.com
|
||||
#DICEBEAR_API_HOSTNAME=
|
||||
|
||||
# Tor Onion URL
|
||||
# The full http:// onion url to add as an 'Onion-Location' header
|
||||
#ONION_URL=
|
||||
|
||||
# Highlights
|
||||
# Highlight up to three profiles on the homepage
|
||||
#KX_HIGHLIGHTS_1_NAME=
|
||||
#KX_HIGHLIGHTS_1_DESCRIPTION=
|
||||
#KX_HIGHLIGHTS_1_FINGERPRINT=
|
||||
|
||||
#KX_HIGHLIGHTS_2_NAME=
|
||||
#KX_HIGHLIGHTS_2_DESCRIPTION=
|
||||
#KX_HIGHLIGHTS_2_FINGERPRINT=
|
||||
|
||||
#KX_HIGHLIGHTS_3_NAME=
|
||||
#KX_HIGHLIGHTS_3_DESCRIPTION=
|
||||
#KX_HIGHLIGHTS_3_FINGERPRINT=
|
||||
|
||||
# Enable caching of keys (experimental)
|
||||
# Opt-in; to disable, omit the environment variable
|
||||
#ENABLE_EXPERIMENTAL_CACHE=true
|
||||
|
||||
# Enable profile request rate limiting (experimental)
|
||||
# Opt-in; to disable, omit the environment variable
|
||||
#ENABLE_EXPERIMENTAL_RATE_LIMITER=true
|
||||
#ENABLE_EXPERIMENTAL_CACHE=
|
||||
|
|
|
@ -66,78 +66,38 @@ describe('browser', function () {
|
|||
})
|
||||
})
|
||||
describe('generateProfileURL()', function () {
|
||||
it('should handle a https WKD URL', async function () {
|
||||
it('should handle a WKD URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'wkd',
|
||||
input: 'test@doip.rocks',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'https'
|
||||
hostname: 'keyoxide.instance'
|
||||
})
|
||||
local.should.equal('https://keyoxide.instance/test@doip.rocks')
|
||||
})
|
||||
it('should handle a http WKD URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'wkd',
|
||||
input: 'test@doip.rocks',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'http'
|
||||
})
|
||||
local.should.equal('http://keyoxide.instance/test@doip.rocks')
|
||||
})
|
||||
it('should handle a https HKP+email URL', async function () {
|
||||
it('should handle a HKP+email URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'hkp',
|
||||
input: 'test@doip.rocks',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'https'
|
||||
hostname: 'keyoxide.instance'
|
||||
})
|
||||
local.should.equal('https://keyoxide.instance/hkp/test@doip.rocks')
|
||||
})
|
||||
it('should handle a http HKP+email URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'hkp',
|
||||
input: 'test@doip.rocks',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'http'
|
||||
})
|
||||
local.should.equal('http://keyoxide.instance/hkp/test@doip.rocks')
|
||||
})
|
||||
it('should handle a https HKP+fingerprint URL', async function () {
|
||||
it('should handle a HKP+fingerprint URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'hkp',
|
||||
input: '3637202523E7C1309AB79E99EF2DC5827B445F4B',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'https'
|
||||
hostname: 'keyoxide.instance'
|
||||
})
|
||||
local.should.equal('https://keyoxide.instance/3637202523E7C1309AB79E99EF2DC5827B445F4B')
|
||||
})
|
||||
it('should handle a http HKP+fingerprint URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'hkp',
|
||||
input: '3637202523E7C1309AB79E99EF2DC5827B445F4B',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'http'
|
||||
})
|
||||
local.should.equal('http://keyoxide.instance/3637202523E7C1309AB79E99EF2DC5827B445F4B')
|
||||
})
|
||||
it('should handle a https keybase URL', async function () {
|
||||
it('should handle a keybase URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'keybase',
|
||||
input: 'https://keybase.io/doip/pgp_keys.asc?fingerprint=3637202523E7C1309AB79E99EF2DC5827B445F4B',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'https'
|
||||
hostname: 'keyoxide.instance'
|
||||
})
|
||||
local.should.equal('https://keyoxide.instance/keybase/doip/3637202523E7C1309AB79E99EF2DC5827B445F4B')
|
||||
})
|
||||
it('should handle a http keybase URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
source: 'keybase',
|
||||
input: 'https://keybase.io/doip/pgp_keys.asc?fingerprint=3637202523E7C1309AB79E99EF2DC5827B445F4B',
|
||||
hostname: 'keyoxide.instance',
|
||||
scheme: 'http'
|
||||
})
|
||||
local.should.equal('http://keyoxide.instance/keybase/doip/3637202523E7C1309AB79E99EF2DC5827B445F4B')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,11 +1,6 @@
|
|||
import 'chai/register-should.js'
|
||||
import esmock from 'esmock'
|
||||
import * as doipjs from 'doipjs'
|
||||
|
||||
import * as utils from '../src/server/utils.js'
|
||||
|
||||
const _env = Object.assign({},process.env)
|
||||
|
||||
describe('server', function () {
|
||||
describe('utils', function () {
|
||||
describe('computeWKDLocalPart()', function () {
|
||||
|
@ -31,89 +26,4 @@ describe('server', function () {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
// NOTE: This is necessarily brittle. If these tests fail
|
||||
// in the future, start looking here for what new behaviour
|
||||
// in the implementation is or isn't getting mocked
|
||||
// appropriately.
|
||||
describe('index', function () {
|
||||
|
||||
describe('generateHKPProfile()', function() {
|
||||
|
||||
let index;
|
||||
let fingerprint;
|
||||
/** @type {import('doipjs').Profile */
|
||||
let profile;
|
||||
|
||||
this.beforeEach(async () => {
|
||||
|
||||
// Common arrangement pieces that don't change per test
|
||||
fingerprint = '79895B2E0F87503F1DDE80B649765D7F0DDD9BD5'
|
||||
process.env.DOMAIN = "keyoxide.org"
|
||||
|
||||
const persona = new doipjs.Persona("test", [new doipjs.Claim('dns:domain.tld?type=TXT')])
|
||||
|
||||
profile = new doipjs.Profile(doipjs.enums.ProfileType.OPENPGP, fingerprint, [persona])
|
||||
|
||||
// mock the appropriate pieces of our dependencies so we
|
||||
// can test just the `keyoxide.url` return value.
|
||||
index = await esmock('../src/server/index.js', {
|
||||
'../src/server/openpgpProfiles.js': {
|
||||
fetchHKP: () => {
|
||||
return Promise.resolve(profile)
|
||||
}
|
||||
},
|
||||
'libravatar': {
|
||||
get_avatar_url: () => {
|
||||
return "example.org/avatar.png"
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.afterEach(() => {
|
||||
process.env = _env
|
||||
})
|
||||
|
||||
it('should handle implicit scheme for keyoxide URL', async function () {
|
||||
|
||||
// Arrange
|
||||
// no setting process.env.SCHEME
|
||||
|
||||
// Act
|
||||
const local = await index.generateHKPProfile(fingerprint)
|
||||
|
||||
// Assert
|
||||
local.verifiers[0].url.should.equal(`https://keyoxide.org/hkp/${fingerprint}`)
|
||||
|
||||
})
|
||||
|
||||
it('should handle explicit http scheme for keyoxide URL', async function () {
|
||||
|
||||
// Arrange
|
||||
process.env.SCHEME = "http"
|
||||
|
||||
// Act
|
||||
const local = await index.generateHKPProfile(fingerprint)
|
||||
|
||||
// Assert
|
||||
local.verifiers[0].url.should.equal(`http://keyoxide.org/hkp/${fingerprint}`)
|
||||
|
||||
})
|
||||
|
||||
it('should handle explicit https scheme for keyoxide URL', async function () {
|
||||
|
||||
// Arrange
|
||||
process.env.SCHEME = "https"
|
||||
|
||||
// Act
|
||||
const local = await index.generateHKPProfile(fingerprint)
|
||||
|
||||
// Assert
|
||||
local.verifiers[0].url.should.equal(`https://keyoxide.org/hkp/${fingerprint}`)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,8 +0,0 @@
|
|||
extends templates/base.pug
|
||||
|
||||
block content
|
||||
h1 429 TOO MANY REQUESTS
|
||||
p
|
||||
| Too many requests from this IP, please try again later.
|
||||
br
|
||||
| Limit: 3 profile requests per second.
|
|
@ -1,48 +0,0 @@
|
|||
extends templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
h1 Apps
|
||||
h2 Keyoxide mobile
|
||||
p The app allows you to verify the online identities of Keyoxide profiles.
|
||||
.screenshots
|
||||
img(src="/static/img/keyoxide_mobile_dark_home.jpg"
|
||||
alt="Screenshot of Keyoxide mobile app")
|
||||
img(src="/static/img/keyoxide_mobile_dark_profile.jpg"
|
||||
alt="Screenshot of Keyoxide mobile app")
|
||||
img(src="/static/img/keyoxide_mobile_light_home.jpg"
|
||||
alt="Screenshot of Keyoxide mobile app")
|
||||
h3 Download
|
||||
.banners
|
||||
a(href="https://f-droid.org/packages/org.keyoxide.keyoxide/")
|
||||
img(src="https://img.shields.io/badge/F--Droid-1976D2?style=for-the-badge&logo=f-droid&logoColor=white",
|
||||
alt="Get it on F-Droid")
|
||||
a(href="https://play.google.com/store/apps/details?id=org.keyoxide.keyoxide&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1")
|
||||
img(src="https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white",
|
||||
alt="Get it on Google Play")
|
||||
a(href="https://apps.apple.com/us/app/keyoxide/id1670664318")
|
||||
img(src="https://img.shields.io/badge/App_Store-0D96F6?style=for-the-badge&logo=app-store&logoColor=white",
|
||||
alt="Get it on App Store")
|
||||
a(href="https://codeberg.org/Berker/keyoxide-flutter/releases")
|
||||
img(src="https://img.shields.io/badge/Codeberg.org-2185d0?style=for-the-badge&logo=codeberg&logoColor=white",
|
||||
alt="Get it on Codeberg.org")
|
||||
p
|
||||
| Developer:
|
||||
a(href="https://keyoxide.org/aspe:keyoxide.org:WHM3OC7UFRARIVEXDXUV4GVXNQ") Berker Sen
|
||||
br
|
||||
| Source code:
|
||||
a(href="https://codeberg.org/Berker/keyoxide-flutter") Codeberg.org
|
||||
|
||||
hr
|
||||
|
||||
h2 Keyoxide ASP web tool
|
||||
p The web tool lets you create and maintain Keyoxide profiles using the ASP method.
|
||||
.screenshots
|
||||
img(src="/static/img/keyoxide_asp_web_home.jpg"
|
||||
alt="Screenshot of Keyoxide ASP web tool")
|
||||
p
|
||||
| Homepage:
|
||||
a(href="https://asp.keyoxide.org") asp.keyoxide.org
|
||||
br
|
||||
| Source code:
|
||||
a(href="https://codeberg.org/keyoxide/kx-aspe-web") Codeberg.org
|
|
@ -1,6 +1,6 @@
|
|||
extends templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.long_form.narrow
|
||||
h1= title
|
||||
| !{ content }
|
||||
.card !{ content }
|
||||
|
|
101
views/index.pug
101
views/index.pug
|
@ -1,44 +1,76 @@
|
|||
extends templates/base.pug
|
||||
|
||||
block content
|
||||
section#search.form-wrapper
|
||||
<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m10.87 0c-0.4551 0-0.832 0.3769-0.832 0.832 0 0.4551 0.3769 0.832 0.832 0.832 0.4551 0 0.832-0.3769 0.832-0.832 0-0.4551-0.3769-0.832-0.832-0.832zm1.85 0.293c-0.5683 0-1.037 0.4688-1.037 1.037 0 0.2368 0.0807 0.4573 0.2168 0.6328-0.05687-0.007139-0.1151-0.01172-0.1738-0.01172-0.7683 0-1.398 0.6302-1.398 1.398 0 0.7683 0.6302 1.4 1.398 1.4 0.3773 0 0.7222-0.1523 0.9746-0.3984-0.00891 0.07397-0.01562 0.1483-0.01562 0.2246 0 0.5999 0.3126 1.131 0.7734 1.48l-2.586 2.012v-0.1152c0-1.817-1.482-3.299-3.299-3.299-1.817 0-3.299 1.482-3.299 3.299v6.828c-1.699e-4 -0.01435-0.001953-0.009604-0.001953 0.03516v5.883c-3.1e-6 1.819 1.482 3.301 3.301 3.301 1.73 0 3.115-1.354 3.25-3.051l3.744 2.494c1.512 1.008 3.566 0.5976 4.574-0.9141 1.008-1.512 0.5976-3.566-0.9141-4.574l-5.014-3.344 5.209-4.053c1.263-0.9824 1.63-2.72 0.9102-4.115-0.006296-0.01221-0.0141-0.02849-0.02148-0.04297 0.2572-0.2824 0.4141-0.657 0.4141-1.066 0-0.8712-0.7128-1.584-1.584-1.584-0.6345 0-1.187 0.3779-1.439 0.9199-0.08446-0.006921-0.1692-0.01237-0.2539-0.01367 0.0014-0.03013 0.003906-0.04019 0.003906-0.08008 0-1.036-0.8484-1.885-1.885-1.885-0.579 5.6e-6 -1.097 0.2653-1.443 0.6797 1.08e-4 -0.007109 0-0.01435 0-0.02148 0-0.3811-0.1542-0.7293-0.4043-0.9824 0.5684 0 1.037-0.4688 1.037-1.037 0-0.5683-0.4688-1.037-1.037-1.037zm-1.85 0.4219c0.06898 0 0.1172 0.04821 0.1172 0.1172s-0.04821 0.1172-0.1172 0.1172c-0.06898 0-0.1172-0.04821-0.1172-0.1172s0.0482-0.1172 0.1172-0.1172zm1.85 0.293c0.1822 0 0.3223 0.14 0.3223 0.3223 0 0.1822-0.14 0.3223-0.3223 0.3223-0.1822 0-0.3223-0.14-0.3223-0.3223 0-0.1822 0.14-0.3223 0.3223-0.3223zm-0.9941 1.658c0.3822 0 0.6836 0.3014 0.6836 0.6836 0 0.3822-0.3014 0.6855-0.6836 0.6855-0.3822 0-0.6836-0.3034-0.6836-0.6855 0-0.3822 0.3014-0.6836 0.6836-0.6836zm2.842 0.7402c0.6502-1.88e-5 1.17 0.5197 1.17 1.17 0 0.05687-0.005159 0.1097-0.005859 0.1914-6.61e-4 0.08173 0.01051 0.21 0.08008 0.3281 0.06857 0.1163 0.1972 0.2073 0.3047 0.2402 0.1075 0.03295 0.1919 0.03178 0.2617 0.03125 0.0602-4.678e-4 0.1197 0.005507 0.1797 0.009766 0.0172 0.6402 0.4194 1.188 0.9824 1.422 0.08721 0.03618 0.1774 0.06577 0.2715 0.08594 0.003893 8.346e-4 0.007816 0.001148 0.01172 0.001953 0.00711 0.001489 0.01434 0.002515 0.02148 0.003906 0.04462 0.008569 0.08895 0.01678 0.1348 0.02148 0.001285 1.341e-4 0.00262-1.31e-4 0.003906 0 0.05184 0.00519 0.1051 0.007812 0.1582 0.007812 0.09398 0 0.1858-0.009457 0.2754-0.02539 0.002495-4.438e-4 0.005321 4.557e-4 0.007812 0 0.003259-5.976e-4 0.006512-0.001335 0.009766-0.001953 0.08604-0.01629 0.1694-0.04058 0.25-0.07031 0.004443-0.001639 0.009247-0.002228 0.01367-0.003906 0.0038-0.00142 0.002259-6.138e-4 0.005859-0.001953 0.5338 1.086 0.2618 2.415-0.7188 3.178l-5.604 4.357a0.3573 0.3573 0 0 0 0.02148 0.5801l5.428 3.617c1.19 0.7933 1.51 2.394 0.7168 3.584-0.7933 1.19-2.394 1.51-3.584 0.7168l-4.25-2.832a0.3573 0.3573 0 0 0-0.5547 0.2969v0.3848c0 1.433-1.153 2.586-2.586 2.586-1.433 0-2.586-1.153-2.586-2.586v-5.883c0 0.02601 0.001373 0.01561 0.001953-0.03125a0.3573 0.3573 0 0 0 0-0.001953 0.3573 0.3573 0 0 0 0-0.001953v-6.828c0-1.43 1.154-2.584 2.584-2.584 1.43 0 2.584 1.154 2.584 2.584v0.8457a0.3573 0.3573 0 0 0 0.5762 0.2832l3.207-2.496s0.07527-0.04914 0.1484-0.1289c0.07317-0.07976 0.1932-0.2171 0.1641-0.4531-0.01519-0.1239-0.1002-0.2811-0.1855-0.3535-0.08537-0.07239-0.1288-0.08753-0.166-0.1133-0.3095-0.2145-0.502-0.5695-0.502-0.9609 0-0.6502 0.5177-1.17 1.168-1.17zm3.574 1.057c0.4851 0 0.8691 0.386 0.8691 0.8711 0 0.3632-0.2156 0.671-0.5273 0.8027-5.5e-4 2.323e-4 -0.001402-2.313e-4 -0.001953 0-0.008517 0.003475-0.0281 0.009073-0.03516 0.01172-0.04666 0.01751-0.09382 0.03145-0.1426 0.04102-0.002029 3.979e-4 -0.003828 0.00157-0.00586 0.001953-0.003817 6.812e-4 -0.007883-6.32e-4 -0.01172 0-0.04722 0.008255-0.09583 0.01367-0.1445 0.01367-0.06064 0-0.1188-0.006076-0.1758-0.01758s-0.1119-0.0289-0.1641-0.05078c-0.0522-0.02188-0.1021-0.04893-0.1484-0.08008s-0.08962-0.06619-0.1289-0.1055-0.07432-0.0826-0.1055-0.1289-0.0582-0.09624-0.08008-0.1484c-0.02188-0.0522-0.03928-0.1071-0.05078-0.1641-0.0115-0.05698-0.01758-0.1151-0.01758-0.1758 1e-5 -0.01046 0.001317-0.032 0.001954-0.04492 7.45e-4 -0.01496 4.8e-4 -0.03018 0.001953-0.04492 0.04418-0.4421 0.4124-0.7812 0.8672-0.7812z"/>
|
||||
</svg>
|
||||
#search.form-wrapper.card
|
||||
h2#searchTitle View a profile
|
||||
form(action="post")
|
||||
input#query(type="search" name="query" required placeholder="Search for a profile")
|
||||
input(type="submit" value="Search")
|
||||
label#searchQuery(for="query") Query for fingerprint or email identifier
|
||||
input#query(type="search" name="query" required placeholder="3637202523e7c1309ab79e99ef2dc5827b445f4b, test@doip.rocks" aria-labelledby="searchTitle searchQuery")
|
||||
|
||||
section
|
||||
h2 About Keyoxide
|
||||
p Keyoxide is a decentralized tool to create and verify decentralized online identities.
|
||||
p Just like passports for real life identities, Keyoxide can be used to verify the online identity of people to make sure one is interacting with whom they intended to be and not an imposter.
|
||||
p Unlike real life passports, Keyoxide works with online identities or "personas", meaning these identities can be anonymous and one can have multiple separate personas to protect their privacy, both online and in real life.
|
||||
input(type="submit" value="View profile")
|
||||
p Or view a
|
||||
a(href="/sig") plaintext signature
|
||||
| profile.
|
||||
|
||||
if highlights.length > 0
|
||||
h2 Highlights
|
||||
.hcards.hcards--highlights
|
||||
each hl in highlights
|
||||
.card.card--small-profile
|
||||
h3.name= hl.name
|
||||
p
|
||||
span.fingerprint= hl.fingerprint
|
||||
br
|
||||
span.details= hl.description
|
||||
.spacer
|
||||
p
|
||||
a(href=`/${hl.fingerprint}`).button.full-width View profile
|
||||
- var n = 0
|
||||
while n < 3-highlights.length
|
||||
.card.card--small-profile-dummy
|
||||
- n++
|
||||
|
||||
h2 About Keyoxide
|
||||
.hcards.hcards--features.hcards--max-2
|
||||
.card
|
||||
h3 Online identity
|
||||
p Verifying online identity with cryptography. View
|
||||
a(href="/project@keyoxide.org") Keyoxide's profile
|
||||
| .
|
||||
.card
|
||||
h3 Mobile app
|
||||
p Available on Android and iOS. More information on
|
||||
a(href="https://mobile.keyoxide.org") mobile.keyoxide.org
|
||||
| .
|
||||
.card
|
||||
h3 Decentralization & privacy
|
||||
p No central server or database. No collected data. Control how your data is stored and accessed.
|
||||
.card
|
||||
h3 Cryptography
|
||||
p Your online identity verifiably signed with widely-used cryptographic standards (OpenPGP, others coming).
|
||||
.card
|
||||
h3 Open Source
|
||||
p All Keyoxide projects are licensed under AGPL-3.0-or-later.
|
||||
.card
|
||||
h3 Transparent funding
|
||||
p Funded by donations. Keyoxide stands against VC and surveillance capitalism.
|
||||
|
||||
h2 Community
|
||||
.card
|
||||
p
|
||||
| Here is what a
|
||||
a(href="/project@keyoxide.org") Keyoxide profile
|
||||
| looks like.
|
||||
p
|
||||
a(href="https://docs.keyoxide.org/getting-started") Get started
|
||||
| and create your own!
|
||||
section
|
||||
h2 Community
|
||||
p
|
||||
| Discussion of the Keyoxide project primarily happens on the
|
||||
a(href="https://matrix.to/#/#keyoxide:matrix.org") #keyoxide Matrix channel
|
||||
| and the
|
||||
| Discussion of the Keyoxide project happens primarily on the
|
||||
a(href="https://community.keyoxide.org") Keyoxide Community Forum
|
||||
| .
|
||||
| The Matrix channel is great for troubleshooting.
|
||||
| The forum is the place to propose new service providers for identity verification, make feature suggestions or report bugs.
|
||||
| . This is the place to propose new service providers for identity verification, make feature suggestions or report bugs.
|
||||
p
|
||||
| There is also the
|
||||
a(href="irc://irc.libera.chat/#keyoxide") #keyoxide:libera.chat IRC room
|
||||
| , the
|
||||
a(href="https://matrix.to/#/#keyoxide:matrix.org") #keyoxide Matrix channel
|
||||
| and the
|
||||
a(href="https://lists.sr.ht/~yarmo/keyoxide-devel") keyoxide-devel mailing list
|
||||
| . The IRC room and Matrix channel are bridged together.
|
||||
p
|
||||
| The project is also present on the fediverse:
|
||||
| The project is also present on the fediverse as
|
||||
a(href="https://fosstodon.org/@keyoxide") @keyoxide@fosstodon.org
|
||||
| .
|
||||
p
|
||||
|
@ -47,10 +79,10 @@ block content
|
|||
| .
|
||||
|
||||
|
||||
section
|
||||
h2 Fund the project
|
||||
h2 Fund the project
|
||||
.card
|
||||
p
|
||||
| The development of Keyoxide and the Decentralized Online Identity Proofs ecosystem is entirely funded by donations.
|
||||
| The development of Keyoxide and the Decentralized OpenPGP Identity Proofs ecosystem is entirely funded by donations.
|
||||
p
|
||||
| The Keyoxide project was awarded a NGI Zero grant from the
|
||||
a(href='https://nlnet.nl/') NLnet Foundation
|
||||
|
@ -58,6 +90,9 @@ block content
|
|||
p
|
||||
| We rely on your support to keep working on Big Tech-independent secure online identity.
|
||||
p
|
||||
a.button.button--donate.button--opencollective(href='https://opencollective.com/keyoxide')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12c2.54 0 4.894-.79 6.834-2.135l-3.107-3.109a7.715 7.715 0 1 1 0-13.512l3.107-3.109A11.943 11.943 0 0 0 12 0zm9.865 5.166l-3.109 3.107A7.67 7.67 0 0 1 19.715 12a7.682 7.682 0 0 1-.959 3.727l3.109 3.107A11.943 11.943 0 0 0 24 12c0-2.54-.79-4.894-2.135-6.834z"></path></svg>
|
||||
| Donate via OpenCollective
|
||||
a.button.button--donate.button--liberapay(href='https://liberapay.com/Keyoxide/')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 32 32"><path d="M3.093 0A3.093 3.093 0 0 0 0 3.093v25.813a3.093 3.093 0 0 0 3.093 3.093h25.813a3.093 3.093 0 0 0 3.093-3.093V3.093A3.093 3.093 0 0 0 28.906 0zm12.276 5.307L12.344 17.85a4.056 4.056 0 0 0-.099.719c-.011.197.031.396.119.572a.926.926 0 0 0 .448.401c.209.104.505.172.881.199l-.652 2.676c-1.031 0-1.844-.129-2.427-.4c-.588-.271-1.011-.636-1.265-1.099a3.245 3.245 0 0 1-.371-1.6a9.035 9.035 0 0 1 .251-1.927l2.765-11.563zm5.204 5.182c.812 0 1.509.125 2.099.371a3.862 3.862 0 0 1 1.448 1.015c.375.428.656.928.839 1.5c.181.573.271 1.188.271 1.839c0 1.057-.172 2.027-.521 2.907a6.915 6.915 0 0 1-1.448 2.276a6.49 6.49 0 0 1-2.224 1.489c-.859.355-1.801.531-2.817.531c-.489 0-.984-.041-1.479-.129l-.98 3.943h-3.224l3.615-15.063a17.858 17.858 0 0 1 1.989-.469c.803-.14 1.615-.213 2.433-.208zm-.417 2.724a6.07 6.07 0 0 0-1.307.131l-1.521 6.333c.245.063.547.088.912.088c.567 0 1.083-.104 1.547-.317a3.463 3.463 0 0 0 1.187-.88a4.07 4.07 0 0 0 .761-1.36a5.308 5.308 0 0 0 .271-1.755c0-.625-.136-1.151-.412-1.589c-.276-.432-.755-.651-1.437-.651z"></path></svg>
|
||||
| Donate via Liberapay
|
||||
a.button.button--donate.button--kofi(href='https://ko-fi.com/keyoxide/')
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 32 32"><path d="M31.844 11.932c-1.032-5.448-6.48-6.125-6.48-6.125H.964C.156 5.807.057 6.87.057 6.87S-.052 16.637.03 22.637c.22 3.228 3.448 3.561 3.448 3.561s11.021-.031 15.953-.067c3.251-.568 3.579-3.423 3.541-4.98c5.808.323 9.896-3.776 8.871-9.219zm-14.751 4.683c-1.661 1.932-5.348 5.297-5.348 5.297s-.161.161-.417.031c-.099-.073-.14-.12-.14-.12c-.595-.588-4.491-4.063-5.381-5.271c-.943-1.287-1.385-3.599-.119-4.948c1.265-1.344 4.005-1.448 5.817.541c0 0 2.083-2.375 4.625-1.281c2.536 1.095 2.443 4.016.963 5.751zm8.23.636c-1.24.156-2.244.036-2.244.036V9.714h2.359s2.631.735 2.631 3.516c0 2.552-1.313 3.557-2.745 4.021z"></path></svg>
|
||||
| Donate via Ko-fi
|
|
@ -1,33 +1,30 @@
|
|||
footer
|
||||
.container
|
||||
p
|
||||
| This site:
|
||||
a(href="/") Homepage
|
||||
| •
|
||||
a(href="/util") Utilities
|
||||
| •
|
||||
a(href="/privacy") Privacy policy
|
||||
br
|
||||
.hcards
|
||||
div
|
||||
h1 Keyoxide
|
||||
a(href="/") Homepage
|
||||
br
|
||||
a(href="/util") Utilities
|
||||
br
|
||||
a(href="/privacy") Privacy policy
|
||||
|
||||
| Keyoxide project:
|
||||
a(href="https://community.keyoxide.org") Forum
|
||||
| •
|
||||
a(href="https://docs.keyoxide.org") Documentation
|
||||
| •
|
||||
a(href="https://blog.keyoxide.org") Blog
|
||||
| •
|
||||
a(href="https://codeberg.org/keyoxide") Source code
|
||||
br
|
||||
div
|
||||
h1 Keyoxide project
|
||||
a(href="https://keyoxide.org") Keyoxide.org
|
||||
br
|
||||
a(href="https://community.keyoxide.org") Community forum
|
||||
br
|
||||
a(href="https://docs.keyoxide.org") Documentation
|
||||
br
|
||||
a(href="https://blog.keyoxide.org") Blog
|
||||
|
||||
| Related:
|
||||
a(href="https://doip.rocks") doip.rocks
|
||||
| •
|
||||
a(href="https://ariadne.id") ariadne.id
|
||||
div
|
||||
h1 Development
|
||||
a(href="https://codeberg.org/keyoxide/") Source code
|
||||
br
|
||||
a(href="https://doip.rocks") doip.rocks
|
||||
br
|
||||
a(href="https://ariadne.id") ariadne.id
|
||||
|
||||
p
|
||||
| Version
|
||||
a(href=meta.keyoxide.sourceUrl)= meta.keyoxide.semver
|
||||
br
|
||||
|
||||
| © 2020-2024 Keyoxide project contributors
|
||||
|
||||
p.copyright © 2022 Keyoxide project contributors
|
|
@ -1,10 +1,9 @@
|
|||
header
|
||||
.container
|
||||
nav
|
||||
a.logo(href='/' aria-label='Home') Keyoxide
|
||||
.spacer
|
||||
.links
|
||||
a.text(href='https://docs.keyoxide.org/getting-started') Getting started
|
||||
a.text(href='/apps') Apps
|
||||
a.text(href='https://docs.keyoxide.org') Docs
|
||||
a.text(href='https://blog.keyoxide.org') Blog
|
||||
a.logo(href='/' aria-label='Home')
|
||||
img(src='/static/img/logo_circle.png' alt='Keyoxide' aria-hidden='true')
|
||||
a.text(href='/') Home
|
||||
a.text(href='https://docs.keyoxide.org') Docs
|
||||
a.text(href='https://blog.keyoxide.org') Blog
|
||||
a.text(href='https://community.keyoxide.org') Forum
|
||||
|
|
|
@ -1,55 +1,46 @@
|
|||
extends templates/base.pug
|
||||
|
||||
mixin generatePersona(persona, isPrimary)
|
||||
if persona.claims.length > 0
|
||||
h2
|
||||
if persona.email
|
||||
| Identity claims (
|
||||
span.p-email #{persona.email}
|
||||
| )
|
||||
else
|
||||
| Identity claims
|
||||
if isPrimary
|
||||
small.primary primary
|
||||
if persona.description
|
||||
span.persona__description.p-comment
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
| #{persona.description}
|
||||
each claim in persona.claims
|
||||
if claim.matches.length > 0
|
||||
kx-claim.kx-item(data-claim=claim,data-status='running')
|
||||
details(aria-label="Claim")
|
||||
summary
|
||||
.info
|
||||
img(src=`https://design.keyoxide.org/brands/service-providers/_/icon.svg` onerror="this.src='https://design.keyoxide.org/brands/service-providers/_/icon.svg'")
|
||||
p
|
||||
span.title= claim.display.profileName
|
||||
span.subtitle-wrapper
|
||||
| [
|
||||
span.subtitle= claim.display.serviceproviderName
|
||||
| ]
|
||||
.icons
|
||||
.verificationStatus
|
||||
.inProgress
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"></path></svg>
|
||||
.success
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
.failure
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
|
||||
.content
|
||||
.subsection
|
||||
img(src='/static/img/link.png')
|
||||
div
|
||||
p Claim link:
|
||||
a(rel="me" href=claim.uri aria-label="Link to claim")= claim.uri
|
||||
mixin generateUser(user, isPrimary)
|
||||
h2
|
||||
span.p-email #{user.userData.email}
|
||||
if isPrimary
|
||||
small.primary primary
|
||||
if user.userData.comment
|
||||
span.p-comment ⓘ #{user.userData.comment}
|
||||
each claim in user.claims
|
||||
if claim.matches.length > 0
|
||||
kx-claim.kx-item(data-claim=claim)
|
||||
details(aria-label="Claim")
|
||||
summary
|
||||
.info
|
||||
p.subtitle= claim.matches[0].serviceprovider.name
|
||||
p.title= claim.matches[0].profile.display
|
||||
.icons
|
||||
.verificationStatus(data-value='running')
|
||||
.inProgress
|
||||
.content
|
||||
.subsection
|
||||
img(src='/static/img/link.png')
|
||||
div
|
||||
if (claim.matches[0].profile.uri)
|
||||
p Profile link:
|
||||
a(rel='me' href=claim.matches[0].profile.uri aria-label="Link to profile")= claim.matches[0].profile.uri
|
||||
else
|
||||
p Profile link: not accessible from browser
|
||||
if (claim.matches[0].proof.uri)
|
||||
p Proof link:
|
||||
a(href=claim.matches[0].proof.uri aria-label="Link to proof")= claim.matches[0].proof.uri
|
||||
else
|
||||
p Proof link: not accessible from browser
|
||||
|
||||
block content
|
||||
if (data && 'publicKey' in data)
|
||||
script.
|
||||
kx = {
|
||||
publicKey: !{JSON.stringify(data.publicKey)}
|
||||
script.
|
||||
kx = {
|
||||
key: {
|
||||
url: "!{data && data.key && data.key.fetchURL ? data.key.fetchURL : null}",
|
||||
object: null
|
||||
}
|
||||
}
|
||||
|
||||
if (data && 'errors' in data && data.errors.length > 0)
|
||||
section
|
||||
|
@ -67,7 +58,7 @@ block content
|
|||
a(href="https://docs.keyoxide.org/getting-started/something-went-wrong/") documentation
|
||||
| for help.
|
||||
else
|
||||
section.profile
|
||||
section.profile.narrow.h-card
|
||||
noscript
|
||||
p Keyoxide requires JavaScript to function.
|
||||
|
||||
|
@ -89,7 +80,12 @@ block content
|
|||
dialog#dialog--verifySignature
|
||||
div
|
||||
form(method='post')
|
||||
label(for="sigVerInput") Signature name
|
||||
label(for="sigVerInput") Signature
|
||||
textarea#sigVerInput.input(name='signature')
|
||||
input.no-margin(type='submit' name='submit' value='VERIFY SIGNATURE')
|
||||
br
|
||||
br
|
||||
label(for="sigVerOutput") Verification result
|
||||
textarea#sigVerOutput.output(name='message' placeholder='Waiting for input' readonly)
|
||||
form(method="dialog")
|
||||
input(type="submit" value="Close")
|
||||
|
@ -111,22 +107,10 @@ block content
|
|||
input(type='submit', name='submit', value='Generate profile')
|
||||
|
||||
unless (isSignature && !signature)
|
||||
if (theme)
|
||||
style
|
||||
| :root {
|
||||
| --primary-color-light: #{theme.primary.light};
|
||||
| --primary-color-dark: #{theme.primary.dark};
|
||||
| --primary-color-subtle-light: #{theme.primarySubtle.light};
|
||||
| --primary-color-subtle-dark: #{theme.primarySubtle.dark};
|
||||
| --background-color-light: #{theme.background.light};
|
||||
| --background-color-dark: #{theme.background.dark};
|
||||
| }
|
||||
.profile__header
|
||||
img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
|
||||
#profileHeader.card.card--transparent.card--profileHeader
|
||||
img#profileAvatar.u-logo(src=data.extra.avatarURL alt="avatar")
|
||||
|
||||
p.profile__name.p-name= data.personas[data.primaryPersonaIndex].name
|
||||
if (data.personas[data.primaryPersonaIndex].description)
|
||||
p= data.personas[data.primaryPersonaIndex].description
|
||||
p#profileName.p-name= data.keyData.users[data.keyData.primaryUserIndex].userData.name
|
||||
|
||||
if (enable_message_encryption || enable_signature_verification)
|
||||
.button-wrapper
|
||||
|
@ -135,33 +119,27 @@ block content
|
|||
if (enable_signature_verification)
|
||||
button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature
|
||||
|
||||
.profile__claims
|
||||
+generatePersona(data.personas[data.primaryPersonaIndex], true && data.personas.length > 1)
|
||||
each persona, index in data.personas
|
||||
unless index == data.primaryPersonaIndex
|
||||
+generatePersona(persona, false)
|
||||
h2 Profile
|
||||
if data.verifiers.length > 0
|
||||
button.themed(onClick=`showQR('${data.verifiers[0].url}', 'profile_verifier_url')` aria-label='Show profile ID QR') Keyoxide profile QR
|
||||
button.themed(onClick=`showQR('${data.identifier}', 'profile_identifier')` aria-label='Show profile ID QR') Profile ID QR
|
||||
+generateUser(data.keyData.users[data.keyData.primaryUserIndex], true)
|
||||
each user, index in data.keyData.users
|
||||
unless index == data.keyData.primaryUserIndex
|
||||
+generateUser(user, false)
|
||||
|
||||
section
|
||||
h2 Profile information
|
||||
if (data && data.publicKey)
|
||||
h3 Public key
|
||||
kx-key.kx-item(data-keydata=data.publicKey)
|
||||
details(aria-label="Key")
|
||||
summary
|
||||
.info
|
||||
p
|
||||
span.title= data.identifier
|
||||
span.subtitle-wrapper
|
||||
| [
|
||||
span.subtitle= data.publicKey.fetch.method
|
||||
| ]
|
||||
.content
|
||||
.subsection
|
||||
img(src='/static/img/link.png')
|
||||
div
|
||||
p Key link:
|
||||
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
|
||||
#profileProofs.card.card--transparent
|
||||
h2 Key
|
||||
kx-key.kx-item(data-keydata=data.keyData)
|
||||
details(aria-label="Key")
|
||||
summary
|
||||
.info
|
||||
p.subtitle= data.keyData.key.fetchMethod
|
||||
p.title= data.keyData.fingerprint
|
||||
.content
|
||||
.subsection
|
||||
img(src='/static/img/link.png')
|
||||
div
|
||||
p Key link:
|
||||
a.u-key(href=data.keyData.key.uri rel="pgpkey" aria-label="Link to cryptographic key")= data.keyData.key.uri
|
||||
hr
|
||||
.subsection
|
||||
img(src='/static/img/qrcode.png')
|
||||
div
|
||||
button(onClick=`showQR('${data.keyData.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR
|
|
@ -6,7 +6,6 @@ html(lang='en')
|
|||
meta(name='theme-color' content='#fff')
|
||||
meta(name='description' content='Modern and secure platform to manage a decentralized identity based on cryptographic keys')
|
||||
link(rel='shortcut icon' href='/favicon.svg')
|
||||
link(rel='stylesheet' href='/static/main.css')
|
||||
title= (title ? title : 'Keyoxide')
|
||||
|
||||
include ../partials/header.pug
|
||||
|
@ -17,7 +16,6 @@ html(lang='en')
|
|||
|
||||
include ../partials/footer.pug
|
||||
|
||||
link(rel='stylesheet' href='/static/main.css')
|
||||
script(type='application/javascript' defer src='/static/openpgp.js' charset='utf-8')
|
||||
script(type='application/javascript' defer src='/static/doipFetchers.js' charset='utf-8')
|
||||
script(type='application/javascript' defer src='/static/doip.js' charset='utf-8')
|
||||
script(type='application/javascript' defer src='/static/main.js' charset='utf-8')
|
|
@ -1,7 +1,7 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.narrow
|
||||
h1 Argon2 utility
|
||||
|
||||
h2 Generate Argon2 hash
|
||||
|
@ -11,7 +11,7 @@ block content
|
|||
a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
|
||||
| hashes useful to
|
||||
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
|
||||
| . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
|
||||
| . Be sure to include "openpgp4fpr:" for a valid proof!
|
||||
h3 Input
|
||||
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
|
||||
h3 Hash
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.narrow
|
||||
h1 bcrypt utility
|
||||
|
||||
h2 Generate bcrypt hash
|
||||
|
@ -11,7 +11,7 @@ block content
|
|||
a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt
|
||||
| hashes useful to
|
||||
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
|
||||
| . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
|
||||
| . Be sure to include "openpgp4fpr:" for a valid proof!
|
||||
h3 Input
|
||||
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
|
||||
h3 Hash
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
h2 Keyoxide utilities
|
||||
section.narrow
|
||||
h1 Keyoxide utilities
|
||||
p
|
||||
a(href="/util/profile-url") Get the URL for a Keyoxide profile
|
||||
p
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.narrow
|
||||
h1 Profile URL
|
||||
form#form-util-profile-url(method='post')
|
||||
p This tool generates an URL for your Keyoxide profile page.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.narrow
|
||||
h1 QR Code
|
||||
form#form-util-qr(method='post')
|
||||
pre
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.narrow
|
||||
h1 QR Code
|
||||
form#form-util-qrfp(method='post')
|
||||
p
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
extends ../templates/base.pug
|
||||
|
||||
block content
|
||||
section
|
||||
section.narrow
|
||||
h1 Web Key Directory URL generator
|
||||
form#form-util-wkd(method='post')
|
||||
p
|
||||
|
|
|
@ -13,8 +13,10 @@ export default (env) => {
|
|||
mode: env.mode,
|
||||
entry: {
|
||||
main: {
|
||||
import: './static-src/index.js'
|
||||
}
|
||||
import: './static-src/index.js',
|
||||
dependOn: 'openpgp',
|
||||
},
|
||||
openpgp: './node_modules/openpgp/dist/openpgp.js',
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
|
@ -25,11 +27,10 @@ export default (env) => {
|
|||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.s[ca]ss$/,
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -61,19 +62,12 @@ export default (env) => {
|
|||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: './static-src/files/', to: '../static/' },
|
||||
{ from: './node_modules/openpgp/dist/openpgp.js', to: '../static/openpgp.js' },
|
||||
{ from: './node_modules/doipjs/dist/doip.core.js', to: '../static/doip.js' },
|
||||
{ from: './node_modules/doipjs/dist/doip.fetchers.minimal.js', to: '../static/doipFetchers.js' },
|
||||
],
|
||||
options: {
|
||||
concurrency: 10,
|
||||
},
|
||||
}),
|
||||
],
|
||||
externals: {
|
||||
doipjs: 'doip',
|
||||
openpgp: 'openpgp'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
|
|
Loading…
Reference in a new issue