Merge upstream

This commit is contained in:
Tyler Beckman 2024-06-02 10:26:32 -06:00
commit 7b5aa4703a
Signed by untrusted user who does not match committer: Ty
GPG key ID: 2813440C772555A4
64 changed files with 9177 additions and 1374 deletions

View file

@ -1,7 +0,0 @@
<!-- 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 -->

View file

@ -0,0 +1,16 @@
---
name: 'Bug'
about: 'Report a bug'
title: '[BUG] '
ref: 'dev'
labels:
- 'Status/Needs Triage'
- Type/Bug
---
### What happened
### Proposed solutions

View file

@ -0,0 +1,8 @@
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.

View file

@ -0,0 +1,12 @@
---
name: 'Enhancement'
about: 'Suggest a new feature or improve an existing one'
title: ''
ref: 'dev'
labels:
- 'Status/Needs Triage'
- Type/Enhancement
---
### Proposal

View file

@ -0,0 +1,8 @@
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.

View file

@ -1,4 +1,4 @@
pipeline: steps:
test: test:
image: node image: node
commands: commands:
@ -18,6 +18,9 @@ pipeline:
from_secret: codeberg_password from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web repo: codeberg.org/keyoxide/keyoxide-web
tags: latest tags: latest
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
build-tag-container: build-tag-container:
when: when:
@ -32,6 +35,9 @@ pipeline:
from_secret: codeberg_password from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web repo: codeberg.org/keyoxide/keyoxide-web
auto_tag: true auto_tag: true
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
build-dev-container: build-dev-container:
when: when:
@ -46,3 +52,6 @@ pipeline:
from_secret: codeberg_password from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web repo: codeberg.org/keyoxide/keyoxide-web
tags: dev tags: dev
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH

View file

@ -6,6 +6,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [3.6.4] - 2023-03-27
### Fixed ### Fixed
- Missing /graphql proxy API endpoint - Missing /graphql proxy API endpoint

View file

@ -1,15 +1,21 @@
FROM node:16-alpine as builder FROM node:20-alpine as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN corepack enable RUN corepack enable
RUN yarn install --immutable RUN yarn install --immutable
RUN yarn run build RUN yarn run build:server && yarn run build:static
### ###
FROM node:16-alpine FROM node:20-alpine
ARG CI_COMMIT_SHA
ARG CI_COMMIT_BRANCH
ENV COMMIT_SHA=$CI_COMMIT_SHA
ENV COMMIT_BRANCH=$CI_COMMIT_BRANCH
WORKDIR /app WORKDIR /app
COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package.json /app/package.json
@ -20,4 +26,4 @@ COPY --from=builder /app/static /app/static
EXPOSE 3000 EXPOSE 3000
CMD node --experimental-fetch ./dist/ CMD node ./dist/

View file

@ -1,15 +1,15 @@
# Keyoxide # keyoxide-web
[![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) [![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=for-the-badge)](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE) [![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=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide) [![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=for-the-badge)](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=for-the-badge)](https://fosstodon.org/@keyoxide) [![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=flat)](https://fosstodon.org/@keyoxide)
[![Liberapay receiving](https://img.shields.io/liberapay/receives/keyoxide?style=for-the-badge)](https://liberapay.com/Keyoxide) [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
[Keyoxide](https://keyoxide.org) is a modern, secure and decentralized platform to prove your online identity. `keyoxide-web` is the web client that powers [keyoxide.org](https://keyoxide.org) and which you can freely host on your own infrastructure.
## Self-hosting ## Hosting keyoxide-web
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! 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,38 +17,50 @@ 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: The Docker container allows you to easily self-host the [Keyoxide](https://keyoxide.org) project. To get started, simply run:
`docker run -d -p 3000:3000 keyoxide/keyoxide:stable` ```sh
docker run -d -p 3000:3000 codeberg.org/keyoxide/keyoxide-web:latest
```
Keyoxide will now be available by visiting http://localhost:3000. Keyoxide will now be available by visiting http://localhost:3000.
More information available in the [documentation](docs.keyoxide.org/self-hosting). 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).
## Contributing ## Contributing
Anyone can contribute if they'd like! No need to be a programmer or technically-oriented for that matter. Anyone can contribute!
Contributing to Keyoxide can happen in many forms: Developers are invited to:
- Finding and reporting bugs - fork the repository and play around
- Suggesting new features - submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/keyoxide-web/issues)
- Improving documentation
- Writing code to fix bugs and features 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.
- Promoting decentralized identity and web3.0
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
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. 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.
### Local development ## About the Keyoxide project
To run Keyoxide locally on your machine for 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!
- install either 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.
- 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 Normal file
View file

@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.2.2/schema.json",
"organizeImports": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": false
}
}

View file

@ -8,21 +8,21 @@ There are no accounts on Keyoxide. Never does Keyoxide need to know any of your
## Profile pages ## Profile pages
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. 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.
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. 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.
OpenPGP keys, profile pages and claim verifications are not cached on the server. Profile pages and claim verifications are not cached on the server. OpenPGP keys may be cached for a minute to alleviate strain on keyservers.
## Donations ## Donations
Donations are handled by Stripe. Keyoxide does not store personal or payment-related information. Donations are handled by OpenCollective. Keyoxide does not store personal or payment-related information.
How Stripe processes the data is covered by the [Stripe privacy policy](https://stripe.com/privacy). How OpenCollective processes the data is covered by the [OpenCollective Privacy Policy](https://opencollective.com/privacypolicy).
## Keyoxide instances ## Keyoxide instances
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. 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.
## Keyoxide.org instance ## Keyoxide.org instance

View file

@ -12,9 +12,6 @@ services:
- 3000:3000 - 3000:3000
environment: environment:
- DOMAIN= - DOMAIN=
# - KX_HIGHLIGHTS_1_NAME=
# - KX_HIGHLIGHTS_1_DESCRIPTION=
# - KX_HIGHLIGHTS_1_FINGERPRINT=
## The hostname to reach the doip_proxy container below ## The hostname to reach the doip_proxy container below
# - PROXY_HOSTNAME= # - PROXY_HOSTNAME=
## The onion URL to advertise in the HTTP response headers ## The onion URL to advertise in the HTTP response headers

16
keyoxide-web.service Normal file
View file

@ -0,0 +1,16 @@
[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

View file

@ -1,9 +1,7 @@
{ {
"execArgs": [
"--experimental-fetch"
],
"env": { "env": {
"NODE_ENV": "development" "NODE_ENV": "development",
"LOG_LEVEL": "debug"
}, },
"ext": "js,json,css,pug,md" "ext": "js,json,css,pug,md"
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "keyoxide-web", "name": "keyoxide-web",
"version": "3.6.4", "version": "4.2.7",
"description": "Verifying online identity with cryptography", "description": "Verifying online identity with cryptography",
"main": "./src/index.js", "main": "./src/index.js",
"type": "module", "type": "module",
@ -9,56 +9,61 @@
"ajv": "^8.6.3", "ajv": "^8.6.3",
"bent": "^7.3.12", "bent": "^7.3.12",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"dialog-polyfill": "^0.5.6", "colorjs.io": "^0.4.5",
"doipjs": "^1.0.0", "doipjs": "^1.2.9",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"express": "^4.17.1", "express": "^4.17.1",
"express-http-context2": "^1.0.0",
"express-rate-limit": "^7.0.1",
"express-validator": "^6.13.0", "express-validator": "^6.13.0",
"fork-awesome": "^1.2.0",
"got": "^11.8.2", "got": "^11.8.2",
"hash-wasm": "^4.9.0", "hash-wasm": "^4.9.0",
"jstransformer-markdown-it": "^3.0.0", "jstransformer-markdown-it": "^3.0.0",
"keyv": "^4.5.0", "keyv": "^4.5.0",
"libravatar": "^3.0.0", "libravatar": "^3.0.0",
"nanoid": "^5.0.1",
"openpgp": "^5.5.0", "openpgp": "^5.5.0",
"pug": "^3.0.0", "pug": "^3.0.2",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"string-replace-middleware": "^1.0.2", "string-replace-middleware": "^1.0.2",
"winston": "^3.8.2" "winston": "^3.8.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.2.2",
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.6.0", "css-loader": "^6.6.0",
"esmock": "^2.5.0",
"license-check-and-add": "^4.0.5", "license-check-and-add": "^4.0.5",
"mini-css-extract-plugin": "^2.5.3", "mini-css-extract-plugin": "^2.5.3",
"mocha": "^10.1.0", "mocha": "^10.1.0",
"nodemon": "^2.0.20", "nodemon": "^3.0.3",
"rome": "^12.1", "sass": "^1.67.0",
"sass-loader": "^13.3.2",
"standard": "^17.0.0", "standard": "^17.0.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"webpack": "^5.75.0", "webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.7.0", "webpack-bundle-analyzer": "^4.7.0",
"webpack-cli": "^5.0.0" "webpack-cli": "^5.0.0"
}, },
"scripts": { "scripts": {
"start": "node --experimental-fetch ./", "start": "node ./",
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev", "dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev",
"test": "yarn run lint && mocha", "test": "yarn run lint && mocha",
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./", "watch": "nodemon --config nodemon.json ./",
"build": "yarn run build:server & yarn run build:static", "build": "yarn run build:server && yarn run build:static",
"build:server": "ncc build ./src/index.js -o dist", "build:server": "ncc build ./src/index.js -o dist",
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production", "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", "build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
"lint": "yarn run standard:check && yarn run rome:check", "lint": "yarn run standard:check && yarn run biome:check",
"standard:check": "./node_modules/.bin/standard ./src", "biome:check": "biome check ./src && biome lint ./src",
"standard:fix": "./node_modules/.bin/standard --fix ./src", "biome:fix": "biome check --apply ./src && biome lint --apply ./src",
"rome:check": "./node_modules/.bin/rome check ./src", "standard:check": "standard ./src",
"rome:fix": "./node_modules/.bin/rome check --apply ./src", "standard:fix": "standard --fix ./src",
"license:check": "./node_modules/.bin/license-check-and-add check", "license:check": "license-check-and-add check",
"license:add": "./node_modules/.bin/license-check-and-add add", "license:add": "license-check-and-add add",
"license:remove": "./node_modules/.bin/license-check-and-add remove" "license:remove": "license-check-and-add remove"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -128,7 +128,13 @@ router.get('/fetch',
data = await doVerification(data) data = await doVerification(data)
} }
try {
data = data.toJSON() data = data.toJSON()
} catch (error) {
data = {
errors: [error.message]
}
}
try { try {
// Validate JSON // Validate JSON
@ -162,7 +168,13 @@ router.get('/verify',
// Do verification // Do verification
let data = await doVerification(profile) let data = await doVerification(profile)
try {
data = data.toJSON() data = data.toJSON()
} catch (error) {
data = {
errors: [error.message]
}
}
try { try {
// Validate JSON // Validate JSON

View file

@ -93,6 +93,40 @@ 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 // DNS route
router.get('/dns', query('domain').isFQDN(), (req, res) => { router.get('/dns', query('domain').isFQDN(), (req, res) => {
const errors = validationResult(req) const errors = validationResult(req)

View file

@ -28,7 +28,10 @@ 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/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
import express from 'express' import express from 'express'
import * as httpContext from 'express-http-context2'
import { nanoid } from 'nanoid'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { execSync } from 'child_process'
import { stringReplace } from 'string-replace-middleware' import { stringReplace } from 'string-replace-middleware'
import * as pug from 'pug' import * as pug from 'pug'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
@ -41,6 +44,20 @@ import staticRoute from './routes/static.js'
import utilRoute from './routes/util.js' import utilRoute from './routes/util.js'
dotenv.config() 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 app = express()
const packageData = JSON.parse(readFileSync('./package.json')) const packageData = JSON.parse(readFileSync('./package.json'))
@ -48,12 +65,24 @@ app.set('env', process.env.NODE_ENV || 'production')
app.engine('pug', pug.__express).set('view engine', 'pug') app.engine('pug', pug.__express).set('view engine', 'pug')
app.set('port', process.env.PORT || 3000) app.set('port', process.env.PORT || 3000)
app.set('domain', process.env.DOMAIN) 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('keyoxide_version', packageData.version)
app.set('git_branch', gitBranch)
app.set('git_hash', gitHash)
app.set('onion_url', process.env.ONION_URL) app.set('onion_url', process.env.ONION_URL)
// Middlewares // Middlewares
app.use(httpContext.middleware)
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'interest-cohort=()') 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() next()
}) })
@ -65,7 +94,8 @@ if (app.get('onion_url')) {
} }
app.use(stringReplace({ app.use(stringReplace({
PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || process.env.DOMAIN || 'null' PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || process.env.DOMAIN || 'null',
PLACEHOLDER__PROXY_SCHEME: process.env.PROXY_SCHEME || process.env.SCHEME || 'https'
}, { }, {
contentTypeFilterRegexp: /application\/javascript/ contentTypeFilterRegexp: /application\/javascript/
})) }))

View file

@ -28,6 +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/>. 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 { createLogger, format, transports } from 'winston'
import * as httpContext from 'express-http-context2'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
dotenv.config() dotenv.config()
@ -37,13 +38,23 @@ const anonymize = format((info, opts) => {
info.keyserver_domain = undefined info.keyserver_domain = undefined
info.username = undefined info.username = undefined
info.fingerprint = undefined info.fingerprint = undefined
info.request_path = undefined
info.request_ip = undefined
} }
return info 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({ const logger = createLogger({
level: process.env.LOG_LEVEL || 'info', level: process.env.LOG_LEVEL || 'info',
format: format.combine( format: format.combine(
addRequestData(),
anonymize(), anonymize(),
format.timestamp(), format.timestamp(),
format.json() format.json()

View file

@ -30,30 +30,23 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import express from 'express' import express from 'express'
import markdownImport from 'markdown-it' import markdownImport from 'markdown-it'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { getMetaFromReq } from '../server/utils.js'
const router = express.Router() const router = express.Router()
const md = markdownImport({ typographer: true }) const md = markdownImport({ typographer: true })
router.get('/', (req, res) => { router.get('/', (req, res) => {
const highlights = [] res.render('index', { meta: getMetaFromReq(req) })
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`]
}) })
}
}
res.render('index', { highlights }) router.get('/apps', (req, res) => {
res.render('apps', { title: 'Apps', meta: getMetaFromReq(req) })
}) })
router.get('/privacy', (req, res) => { router.get('/privacy', (req, res) => {
const rawContent = readFileSync('./content/privacy-policy.md', 'utf8') const rawContent = readFileSync('./content/privacy-policy.md', 'utf8')
const content = md.render(rawContent) const content = md.render(rawContent)
res.render('article', { title: 'Privacy policy', content }) res.render('article', { title: 'Privacy policy', content, meta: getMetaFromReq(req) })
}) })
router.get('/.well-known/webfinger', (req, res) => { router.get('/.well-known/webfinger', (req, res) => {
@ -74,6 +67,12 @@ router.get('/.well-known/webfinger', (req, res) => {
res.json(body) 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) => { router.get('/users/keyoxide', (req, res) => {
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) { if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>') res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')

View file

@ -29,17 +29,47 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/ */
import express from 'express' import express from 'express'
import bodyParserImport from 'body-parser' import bodyParserImport from 'body-parser'
import { rateLimit } from 'express-rate-limit'
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js' import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
import { Profile } from 'doipjs' import { Profile } from 'doipjs'
import { generateProfileTheme, getMetaFromReq, escapedParam } from '../server/utils.js'
import logger from '../log.js'
const router = express.Router() const router = express.Router()
const bodyParser = bodyParserImport.urlencoded({ extended: false }) const bodyParser = bodyParserImport.urlencoded({ extended: false })
router.get('/sig', (req, res) => { let profileRateLimiter = (req, res, next) => {
res.render('profile', { isSignature: true, signature: null }) next()
}
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) => { 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.post('/sig',
profileRateLimiter,
bodyParser,
async (req, res) => {
const data = await generateSignatureProfile(req.body.signature) const data = await generateSignatureProfile(req.body.signature)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier) res.set('ariadne-identity-proof', data.identifier)
@ -49,11 +79,15 @@ router.post('/sig', bodyParser, async (req, res) => {
isSignature: true, isSignature: true,
signature: req.body.signature, signature: req.body.signature,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false,
meta: getMetaFromReq(req)
}) })
}) })
router.get('/wkd/:id', async (req, res) => { router.get('/wkd/:id',
profileRateLimiter,
escapedParam('id'),
async (req, res) => {
const data = await generateWKDProfile(req.params.id) const data = await generateWKDProfile(req.params.id)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier) res.set('ariadne-identity-proof', data.identifier)
@ -61,11 +95,15 @@ router.get('/wkd/:id', async (req, res) => {
title, title,
data: data instanceof Profile ? data.toJSON() : data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false,
meta: getMetaFromReq(req)
}) })
}) })
router.get('/hkp/:id', async (req, res) => { router.get('/hkp/:id',
profileRateLimiter,
escapedParam('id'),
async (req, res) => {
const data = await generateHKPProfile(req.params.id) const data = await generateHKPProfile(req.params.id)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier) res.set('ariadne-identity-proof', data.identifier)
@ -73,11 +111,16 @@ router.get('/hkp/:id', async (req, res) => {
title, title,
data: data instanceof Profile ? data.toJSON() : data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false,
meta: getMetaFromReq(req)
}) })
}) })
router.get('/hkp/:server/:id', async (req, res) => { router.get('/hkp/:server/:id',
profileRateLimiter,
escapedParam('server'),
escapedParam('id'),
async (req, res) => {
const data = await generateHKPProfile(req.params.id, req.params.server) const data = await generateHKPProfile(req.params.id, req.params.server)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier) res.set('ariadne-identity-proof', data.identifier)
@ -85,11 +128,16 @@ router.get('/hkp/:server/:id', async (req, res) => {
title, title,
data: data instanceof Profile ? data.toJSON() : data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false,
meta: getMetaFromReq(req)
}) })
}) })
router.get('/keybase/:username/:fingerprint', async (req, res) => { router.get('/keybase/:username/:fingerprint',
profileRateLimiter,
escapedParam('username'),
escapedParam('fingerprint'),
async (req, res) => {
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint) const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier) res.set('ariadne-identity-proof', data.identifier)
@ -97,19 +145,26 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => {
title, title,
data: data instanceof Profile ? data.toJSON() : data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false,
meta: getMetaFromReq(req)
}) })
}) })
router.get('/:id', async (req, res) => { router.get('/:id',
profileRateLimiter,
escapedParam('id'),
async (req, res) => {
const data = await generateAutoProfile(req.params.id) const data = await generateAutoProfile(req.params.id)
const theme = generateProfileTheme(data)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data: data instanceof Profile ? data.toJSON() : data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false,
theme,
meta: getMetaFromReq(req)
}) })
}) })

View file

@ -28,52 +28,65 @@ 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/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
import express from 'express' import express from 'express'
import { escapedParam, getMetaFromReq } from '../server/utils.js'
const router = express.Router() const router = express.Router()
router.get('/', function (req, res) { router.get('/', function (req, res) {
res.render('util/index') res.render('util/index', { meta: getMetaFromReq(req) })
}) })
router.get('/profile-url', function (req, res) { router.get('/profile-url', function (req, res) {
res.render('util/profile-url') res.render('util/profile-url', { meta: getMetaFromReq(req) })
}) })
router.get('/profile-url/:input', function (req, res) { router.get('/profile-url/:input',
res.render('util/profile-url', { input: req.params.input }) escapedParam('input'),
function (req, res) {
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
}) })
router.get('/qr', function (req, res) { router.get('/qr', function (req, res) {
res.render('util/qr') res.render('util/qr', { meta: getMetaFromReq(req) })
}) })
router.get('/qr/:input', function (req, res) { router.get('/qr/:input',
res.render('util/qr', { input: req.params.input }) escapedParam('input'),
function (req, res) {
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
}) })
router.get('/qrfp', function (req, res) { router.get('/qrfp', function (req, res) {
res.render('util/qrfp') res.render('util/qrfp', { meta: getMetaFromReq(req) })
}) })
router.get('/qrfp/:input', function (req, res) { router.get('/qrfp/:input',
res.render('util/qrfp', { input: req.params.input }) escapedParam('input'),
function (req, res) {
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
}) })
router.get('/wkd', function (req, res) { router.get('/wkd', function (req, res) {
res.render('util/wkd') res.render('util/wkd', { meta: getMetaFromReq(req) })
}) })
router.get('/wkd/:input', function (req, res) { router.get('/wkd/:input',
res.render('util/wkd', { input: req.params.input }) escapedParam('input'),
function (req, res) {
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
}) })
router.get('/argon2', function (req, res) { router.get('/argon2', function (req, res) {
res.render('util/argon2') res.render('util/argon2', { meta: getMetaFromReq(req) })
}) })
router.get('/argon2/:input', function (req, res) { router.get('/argon2/:input',
res.render('util/argon2', { input: req.params.input }) escapedParam('input'),
function (req, res) {
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
}) })
router.get('/bcrypt', function (req, res) { router.get('/bcrypt', function (req, res) {
res.render('util/bcrypt') res.render('util/bcrypt', { meta: getMetaFromReq(req) })
}) })
router.get('/bcrypt/:input', function (req, res) { router.get('/bcrypt/:input',
res.render('util/bcrypt', { input: req.params.input }) escapedParam('input'),
function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
}) })
export default router export default router

View file

@ -157,6 +157,10 @@ export const personaSchema = {
description: 'URL to an avatar image', description: 'URL to an avatar image',
type: ['string', 'null'] type: ['string', 'null']
}, },
themeColor: {
description: 'Profile page theme color',
type: ['string', 'null']
},
isRevoked: { isRevoked: {
type: 'boolean' type: 'boolean'
}, },
@ -215,17 +219,25 @@ export const claimSchema = {
display: { display: {
type: 'object', type: 'object',
properties: { properties: {
name: { profileName: {
type: 'string', type: 'string',
description: 'Account name to display in the user interface' description: 'Account name to display in the user interface'
}, },
url: { profileUrl: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'URL to link to in the user interface' 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: { serviceProviderName: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Name of the service provider to display in the user interface' description: 'Name of the service provider to display in the user interface'
},
serviceProviderId: {
type: ['string', 'null'],
description: 'Id of the service provider'
} }
} }
} }

View file

@ -38,7 +38,9 @@ const generateAspeProfile = async (id) => {
return doipjs.asp.fetchASPE(id) return doipjs.asp.fetchASPE(id)
.then(profile => { .then(profile => {
profile.addVerifier('keyoxide', `https://${process.env.DOMAIN}/${id}`) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
}
profile = processAspProfile(profile) profile = processAspProfile(profile)
return profile return profile
}) })
@ -58,7 +60,9 @@ const generateWKDProfile = async (id) => {
return fetchWKD(id) return fetchWKD(id)
.then(async profile => { .then(async profile => {
profile.addVerifier('keyoxide', `https://${process.env.DOMAIN}/wkd/${id}`) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
}
profile = processOpenPgpProfile(profile) profile = processOpenPgpProfile(profile)
logger.debug('Generating a WKD profile', logger.debug('Generating a WKD profile',
@ -84,12 +88,14 @@ const generateHKPProfile = async (id, keyserverDomain) => {
.then(async profile => { .then(async profile => {
let keyoxideUrl let keyoxideUrl
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') { if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
keyoxideUrl = `https://${process.env.DOMAIN}/hkp/${id}` keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}`
} else { } else {
keyoxideUrl = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}` keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
} }
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', keyoxideUrl) profile.addVerifier('keyoxide', keyoxideUrl)
}
profile = processOpenPgpProfile(profile) profile = processOpenPgpProfile(profile)
logger.debug('Generating a HKP profile', logger.debug('Generating a HKP profile',
@ -111,6 +117,7 @@ const generateAutoProfile = async (id) => {
let result let result
const aspeRe = /aspe:(.*):(.*)/ const aspeRe = /aspe:(.*):(.*)/
const openpgpRe = /openpgp4fpr:(.*)/
if (aspeRe.test(id)) { if (aspeRe.test(id)) {
result = await generateAspeProfile(id) result = await generateAspeProfile(id)
@ -120,6 +127,15 @@ const generateAutoProfile = async (id) => {
} }
} }
if (openpgpRe.test(id)) {
const match = id.match(openpgpRe)
result = await generateHKPProfile(match[1])
if (result && !('errors' in result)) {
return result
}
}
if (id.includes('@')) { if (id.includes('@')) {
result = await generateWKDProfile(id) result = await generateWKDProfile(id)
@ -168,7 +184,9 @@ const generateKeybaseProfile = async (username, fingerprint) => {
return fetchKeybase(username, fingerprint) return fetchKeybase(username, fingerprint)
.then(async profile => { .then(async profile => {
profile.addVerifier('keyoxide', `https://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
}
profile = processOpenPgpProfile(profile) profile = processOpenPgpProfile(profile)
logger.debug('Generating a Keybase profile', logger.debug('Generating a Keybase profile',
@ -215,7 +233,8 @@ const processAspProfile = async (/** @type {import('doipjs').Profile */ profile)
// Overwrite avatarUrl // Overwrite avatarUrl
// TODO: don't overwrite avatarUrl once it's fully supported // TODO: don't overwrite avatarUrl once it's fully supported
profile.personas[profile.primaryPersonaIndex].avatarUrl = `https://api.dicebear.com/6.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128` 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 profile
} }
@ -254,6 +273,14 @@ const processOpenPgpProfile = async (/** @type {import('doipjs').Profile */ prof
return profile return profile
} }
const getScheme = () => {
return process.env.PROXY_SCHEME
? process.env.PROXY_SCHEME
: process.env.SCHEME
? process.env.SCHEME
: 'https'
}
export { generateAspeProfile } export { generateAspeProfile }
export { generateWKDProfile } export { generateWKDProfile }
export { generateHKPProfile } export { generateHKPProfile }

View file

@ -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 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/>. 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 got from 'got'
import * as doipjs from 'doipjs' import * as doipjs from 'doipjs'
import { readKey } from 'openpgp' import { readKey } from 'openpgp'
@ -34,22 +35,31 @@ import { computeWKDLocalPart } from './utils.js'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import Keyv from 'keyv' import Keyv from 'keyv'
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null 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) => { const fetchWKD = (id) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
logger.debug('Fetching an OpenPGP profile via WKD',
{ component: 'wkd_profile_fetcher', action: 'start', profile_id: id })
let publicKey = null let publicKey = null
let profile = null let profile = null
let fetchURL = null let fetchURL = null
if (!id.includes('@')) { if (!id.includes('@')) {
reject(new Error(`The WKD identifier "${id}" is invalid`)) return reject(new Error(`The WKD identifier "${id}" is invalid`))
} }
const [, localPart, domain] = /([^@]*)@(.*)/.exec(id) const [, localPart, domain] = /([^@]*)@(.*)/.exec(id)
if (!(localPart && domain)) { if (!(localPart && domain)) {
reject(new Error(`The WKD identifier "${id}" is invalid`)) return reject(new Error(`The WKD identifier "${id}" is invalid`))
} }
const localEncoded = await computeWKDLocalPart(localPart) const localEncoded = await computeWKDLocalPart(localPart)
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}` const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
@ -58,7 +68,12 @@ const fetchWKD = (id) => {
const hash = createHash('md5').update(id).digest('hex') const hash = createHash('md5').update(id).digest('hex')
if (c && await c.get(hash)) { if (c && await c.get(hash)) {
profile = doipjs.Claim.fromJson(JSON.parse(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) { if (!profile) {
@ -71,7 +86,10 @@ const fetchWKD = (id) => {
return null return null
} }
}) })
} catch (e) { } 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 { try {
plaintext = await got(urlDirect).then((response) => { plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
@ -81,13 +99,16 @@ const fetchWKD = (id) => {
return null return null
} }
}) })
} catch (error) { } catch (errorDirect) {
reject(new Error('No public keys could be fetched using WKD')) 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) { if (!plaintext) {
reject(new Error('No public keys could be fetched using WKD')) return reject(new Error('No public keys could be fetched using WKD'))
} }
try { try {
@ -95,14 +116,25 @@ const fetchWKD = (id) => {
binaryKey: plaintext binaryKey: plaintext
}) })
} catch (error) { } catch (error) {
reject(new Error('No public keys could be read from the data fetched using WKD')) 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) { if (!publicKey) {
reject(new Error('No public keys could be read from the data fetched using WKD')) return reject(new Error('No public keys could be read from the data fetched using WKD'))
} }
try {
profile = await doipjs.openpgp.parsePublicKey(publicKey) 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.method = 'wkd'
profile.publicKey.fetch.query = id profile.publicKey.fetch.query = id
profile.publicKey.fetch.resolvedUrl = fetchURL profile.publicKey.fetch.resolvedUrl = fetchURL
@ -110,8 +142,14 @@ const fetchWKD = (id) => {
if (c && plaintext instanceof Uint8Array) { if (c && plaintext instanceof Uint8Array) {
await c.set(hash, JSON.stringify(profile), 60 * 1000) 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) resolve(profile)
})() })()
}) })
@ -120,6 +158,9 @@ const fetchWKD = (id) => {
const fetchHKP = (id, keyserverDomain) => { const fetchHKP = (id, keyserverDomain) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (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 profile = null
let fetchURL = null let fetchURL = null
@ -142,20 +183,27 @@ const fetchHKP = (id, keyserverDomain) => {
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex') const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
if (c && await c.get(hash)) { if (c && await c.get(hash)) {
profile = doipjs.Claim.fromJson(JSON.parse(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) { if (!profile) {
try { try {
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized) profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
} catch (error) { } 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 profile = null
} }
} }
if (!profile) { if (!profile) {
reject(new Error('No public keys could be fetched using HKP')) return reject(new Error('No public keys could be fetched using HKP'))
return
} }
profile.publicKey.fetch.method = 'hkp' profile.publicKey.fetch.method = 'hkp'
@ -164,8 +212,14 @@ const fetchHKP = (id, keyserverDomain) => {
if (c && profile instanceof doipjs.Profile) { if (c && profile instanceof doipjs.Profile) {
await c.set(hash, JSON.stringify(profile), 60 * 1000) 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) resolve(profile)
})() })()
}) })
@ -181,12 +235,12 @@ const fetchSignature = (signature) => {
profile = await doipjs.signatures.parse(signature) profile = await doipjs.signatures.parse(signature)
// TODO Find the URL to the key // TODO Find the URL to the key
} catch (error) { } catch (error) {
reject(new Error(`Signature could not be properly read (${error.message})`)) return reject(new Error(`Signature could not be properly read (${error.message})`))
} }
// Check if a key was fetched // Check if a key was fetched
if (!profile) { if (!profile) {
reject(new Error('No profile could be fetched')) return reject(new Error('No profile could be fetched'))
} }
resolve(profile) resolve(profile)
@ -204,11 +258,11 @@ const fetchKeybase = (username, fingerprint) => {
profile = await doipjs.openpgp.fetchKeybase(username, fingerprint) profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}` fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
} catch (error) { } catch (error) {
reject(new Error('No public keys could be fetched from Keybase')) return reject(new Error('No public keys could be fetched from Keybase'))
} }
if (!profile) { if (!profile) {
reject(new Error('No public keys could be fetched from Keybase')) return reject(new Error('No public keys could be fetched from Keybase'))
} }
profile.publicKey.fetch.method = 'http' profile.publicKey.fetch.method = 'http'

View file

@ -28,6 +28,9 @@ 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/>. 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 { webcrypto as crypto } from 'crypto'
import { Profile } from 'doipjs'
import Color from 'colorjs.io'
import { param } from 'express-validator'
export async function computeWKDLocalPart (localPart) { export async function computeWKDLocalPart (localPart) {
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase()) const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
@ -78,3 +81,98 @@ export function encodeZBase32 (data) {
} }
return result 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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\//g, '&#x2F;')
.replace(/\\/g, '&#x5C;')
.replace(/`/g, '&#96;'))
}

View file

@ -0,0 +1,7 @@
<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>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -35,8 +35,8 @@ import * as ui from './ui.js'
import * as utils from './utils.js' import * as utils from './utils.js'
// Import CSS files // Import CSS files
import './styles.css' import './styles.scss'
import './kx-styles.css' import './kx-styles.scss'
// Add functions to window // Add functions to window
window.showQR = utils.showQR window.showQR = utils.showQR

View file

@ -45,11 +45,12 @@ export class Claim extends HTMLElement {
} }
async verify() { async verify() {
const claim = doipjs.Claim.fromJson(JSON.parse(this.getAttribute('data-claim'))); const claim = doipjs.Claim.fromJSON(JSON.parse(this.getAttribute('data-claim')));
await claim.verify({ await claim.verify({
proxy: { proxy: {
policy: 'adaptive', policy: 'adaptive',
hostname: 'PLACEHOLDER__PROXY_HOSTNAME' hostname: 'PLACEHOLDER__PROXY_HOSTNAME',
scheme: 'PLACEHOLDER__PROXY_SCHEME'
} }
}); });
this.setAttribute('data-claim', JSON.stringify(claim)); this.setAttribute('data-claim', JSON.stringify(claim));
@ -57,19 +58,24 @@ export class Claim extends HTMLElement {
updateContent(value) { updateContent(value) {
const root = this; const root = this;
const claim = doipjs.Claim.fromJson(JSON.parse(value)); const claimJson = JSON.parse(value);
const claim = doipjs.Claim.fromJSON(claimJson);
root.querySelector('.info .subtitle').innerText = claim.matches[0].about.name; root.querySelector('.info .title').innerText = claimJson.display.profileName;
root.querySelector('.info .title').innerText = claim.matches[0].profile.display; 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`);
try { try {
if (claim.status >= 200) { if (claim.status >= 200) {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.status < 300 ? 'success' : 'failed'); root.setAttribute('data-status', claim.status < 300 ? 'success' : 'failed');
} else { } else {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running'); root.setAttribute('data-status', 'running');
} }
} catch (error) { } catch (error) {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed'); root.setAttribute('data-status', 'failed');
} }
const elContent = root.querySelector('.content'); const elContent = root.querySelector('.content');
@ -102,15 +108,15 @@ export class Claim extends HTMLElement {
const subsection_links_text = subsection_links.appendChild(document.createElement('div')); const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p')); const profile_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].profile.uri) { if (claimJson.display.profileUrl) {
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>`; profile_link.innerHTML = `Profile link: <a rel="me" href="${claimJson.display.profileUrl}" aria-label="link to profile">${claimJson.display.profileUrl}</a>`;
} else { } else {
profile_link.innerHTML = `Profile link: not accessible from browser`; profile_link.innerHTML = `Profile link: not accessible from browser`;
} }
const proof_link = subsection_links_text.appendChild(document.createElement('p')); const proof_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].proof.request.uri) { if (claimJson.display.proofUrl) {
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.request.uri}" aria-label="link to profile">${claim.matches[0].proof.request.uri}</a>`; proof_link.innerHTML = `Proof link: <a href="${claimJson.display.proofUrl}" aria-label="link to profile">${claimJson.display.proofUrl}</a>`;
} else { } else {
proof_link.innerHTML = `Proof link: not accessible from browser`; proof_link.innerHTML = `Proof link: not accessible from browser`;
} }
@ -182,7 +188,7 @@ export class Claim extends HTMLElement {
const subsection_info_text = subsection_info.appendChild(document.createElement('div')); const subsection_info_text = subsection_info.appendChild(document.createElement('div'));
const result_proxyUsed = subsection_info_text.appendChild(document.createElement('p')); const result_proxyUsed = subsection_info_text.appendChild(document.createElement('p'));
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>`; 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>`;
} }
// TODO Display errors // TODO Display errors

View file

@ -1,264 +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 {
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);
}
}

284
static-src/kx-styles.scss Normal file
View file

@ -0,0 +1,284 @@
/*
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);
}
}
}

View file

@ -1,656 +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 '../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--opencollective {
color: #fff;
background-color: #0c2d66;
}
a.button--donate.button--opencollective svg {
fill: #fff;
}
a.button--donate.button--opencollective:hover {
color: #fff;
background-color: #144aa9;
}
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;
}

102
static-src/styles.scss Normal file
View file

@ -0,0 +1,102 @@
/*
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/>.
*/
@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;
}
}
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;
}

View file

@ -0,0 +1,175 @@
/*
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;
}
}
}

View file

@ -0,0 +1,220 @@
/*
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;
}
}
}

View file

@ -0,0 +1,146 @@
/*
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: '- ';
}

176
static-src/styles/vars.scss Normal file
View file

@ -0,0 +1,176 @@
/*
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);
}
}

View file

@ -27,7 +27,6 @@ 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 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/>. 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 QRCode from 'qrcode'
import * as openpgp from 'openpgp' import * as openpgp from 'openpgp'
import * as utils from './utils.js' import * as utils from './utils.js'
@ -51,17 +50,6 @@ const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-
// Initialize UI elements and event listeners // Initialize UI elements and event listeners
export function init() { 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 // Run context-dependent scripts
if (elFormEncrypt) { if (elFormEncrypt) {
runEncryptionForm() runEncryptionForm()
@ -349,8 +337,8 @@ const runArgon2GenerationUtility = () => {
elFeedback.innerHTML = ""; elFeedback.innerHTML = "";
} else { } else {
let feedbackContent = ""; let feedbackContent = "";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[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>"; feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
} }
if (!(elInput.value === elInput.value.toLowerCase())) { if (!(elInput.value === elInput.value.toLowerCase())) {
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>"; feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
@ -391,7 +379,7 @@ window.kx__fixArgon2Input = () => {
const elInput = document.querySelector('#form-util-argon2-generate .input'); const elInput = document.querySelector('#form-util-argon2-generate .input');
elInput.value = elInput.value.toLowerCase(); elInput.value = elInput.value.toLowerCase();
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
elInput.value = `openpgp4fpr:${elInput.value}`; elInput.value = `openpgp4fpr:${elInput.value}`;
} }
@ -414,8 +402,8 @@ const runBcryptGenerationUtility = () => {
elFeedback.innerHTML = ""; elFeedback.innerHTML = "";
} else { } else {
let feedbackContent = ""; let feedbackContent = "";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[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>"; feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
} }
if (!(elInput.value === elInput.value.toLowerCase())) { if (!(elInput.value === elInput.value.toLowerCase())) {
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>"; feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
@ -456,7 +444,7 @@ window.kx__fixBcryptInput = () => {
const elInput = document.querySelector('#form-util-bcrypt-generate .input'); const elInput = document.querySelector('#form-util-bcrypt-generate .input');
elInput.value = elInput.value.toLowerCase(); elInput.value = elInput.value.toLowerCase();
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
elInput.value = `openpgp4fpr:${elInput.value}`; elInput.value = `openpgp4fpr:${elInput.value}`;
} }

View file

@ -46,19 +46,20 @@ export async function computeWKDLocalPart(localPart) {
// Generate Keyoxide profile URL // Generate Keyoxide profile URL
export async function generateProfileURL(data) { export async function generateProfileURL(data) {
let hostname = data.hostname || window.location.hostname; let hostname = data.hostname || window.location.hostname;
let scheme = data.scheme || window.location.protocol.slice(0,-1);
if (data.input == "") { if (data.input == "") {
return "Waiting for input…"; return "Waiting for input…";
} }
switch (data.source) { switch (data.source) {
case "wkd": case "wkd":
return `https://${hostname}/${data.input}`; return `${scheme}://${hostname}/${data.input}`;
break; break;
case "hkp": case "hkp":
if (/.*@.*\..*/.test(data.input)) { if (/.*@.*\..*/.test(data.input)) {
return `https://${hostname}/hkp/${data.input}`; return `${scheme}://${hostname}/hkp/${data.input}`;
} else { } else {
return `https://${hostname}/${data.input}`; return `${scheme}://${hostname}/${data.input}`;
} }
break; break;
case "keybase": case "keybase":
@ -67,7 +68,7 @@ export async function generateProfileURL(data) {
return "Incorrect Keybase public key URL."; return "Incorrect Keybase public key URL.";
} }
const match = data.input.match(re); const match = data.input.match(re);
return `https://${hostname}/keybase/${match[1]}/${match[2]}`; return `${scheme}://${hostname}/keybase/${match[1]}/${match[2]}`;
break; break;
} }
} }

View file

@ -12,28 +12,22 @@
# $ while read -r line; do echo -nE "$line\n" ; done < public.pem > public-oneline.pem # $ while read -r line; do echo -nE "$line\n" ; done < public.pem > public-oneline.pem
#ACTIVITYPUB_PUBLIC_KEY= #ACTIVITYPUB_PUBLIC_KEY=
# Domain for DOIP Proxy server # Domain for Keyoxide Proxy server
# Source code for the server can be found here https://codeberg.org/keyoxide/doip-proxy # To host a Keyoxide Proxy server, refer to https://docs.keyoxide.org/self-hosting/
#PROXY_HOSTNAME= #PROXY_HOSTNAME=
# Domain for Dicebear API server
# Defaults to: api.dicebear.com
#DICEBEAR_API_HOSTNAME=
# Tor Onion URL # Tor Onion URL
# The full http:// onion url to add as an 'Onion-Location' header # The full http:// onion url to add as an 'Onion-Location' header
#ONION_URL= #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) # Enable caching of keys (experimental)
# Opt-in; to disable, omit the environment variable # Opt-in; to disable, omit the environment variable
#ENABLE_EXPERIMENTAL_CACHE= #ENABLE_EXPERIMENTAL_CACHE=true
# Enable profile request rate limiting (experimental)
# Opt-in; to disable, omit the environment variable
#ENABLE_EXPERIMENTAL_RATE_LIMITER=true

View file

@ -66,38 +66,78 @@ describe('browser', function () {
}) })
}) })
describe('generateProfileURL()', function () { describe('generateProfileURL()', function () {
it('should handle a WKD URL', async function () { it('should handle a https WKD URL', async function () {
const local = await utils.generateProfileURL({ const local = await utils.generateProfileURL({
source: 'wkd', source: 'wkd',
input: 'test@doip.rocks', input: 'test@doip.rocks',
hostname: 'keyoxide.instance' hostname: 'keyoxide.instance',
scheme: 'https'
}) })
local.should.equal('https://keyoxide.instance/test@doip.rocks') local.should.equal('https://keyoxide.instance/test@doip.rocks')
}) })
it('should handle a HKP+email URL', async function () { 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 () {
const local = await utils.generateProfileURL({ const local = await utils.generateProfileURL({
source: 'hkp', source: 'hkp',
input: 'test@doip.rocks', input: 'test@doip.rocks',
hostname: 'keyoxide.instance' hostname: 'keyoxide.instance',
scheme: 'https'
}) })
local.should.equal('https://keyoxide.instance/hkp/test@doip.rocks') local.should.equal('https://keyoxide.instance/hkp/test@doip.rocks')
}) })
it('should handle a HKP+fingerprint URL', async function () { 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 () {
const local = await utils.generateProfileURL({ const local = await utils.generateProfileURL({
source: 'hkp', source: 'hkp',
input: '3637202523E7C1309AB79E99EF2DC5827B445F4B', input: '3637202523E7C1309AB79E99EF2DC5827B445F4B',
hostname: 'keyoxide.instance' hostname: 'keyoxide.instance',
scheme: 'https'
}) })
local.should.equal('https://keyoxide.instance/3637202523E7C1309AB79E99EF2DC5827B445F4B') local.should.equal('https://keyoxide.instance/3637202523E7C1309AB79E99EF2DC5827B445F4B')
}) })
it('should handle a keybase URL', async function () { 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 () {
const local = await utils.generateProfileURL({ const local = await utils.generateProfileURL({
source: 'keybase', source: 'keybase',
input: 'https://keybase.io/doip/pgp_keys.asc?fingerprint=3637202523E7C1309AB79E99EF2DC5827B445F4B', input: 'https://keybase.io/doip/pgp_keys.asc?fingerprint=3637202523E7C1309AB79E99EF2DC5827B445F4B',
hostname: 'keyoxide.instance' hostname: 'keyoxide.instance',
scheme: 'https'
}) })
local.should.equal('https://keyoxide.instance/keybase/doip/3637202523E7C1309AB79E99EF2DC5827B445F4B') 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')
})
}) })
}) })
}) })

View file

@ -1,6 +1,11 @@
import 'chai/register-should.js' import 'chai/register-should.js'
import esmock from 'esmock'
import * as doipjs from 'doipjs'
import * as utils from '../src/server/utils.js' import * as utils from '../src/server/utils.js'
const _env = Object.assign({},process.env)
describe('server', function () { describe('server', function () {
describe('utils', function () { describe('utils', function () {
describe('computeWKDLocalPart()', function () { describe('computeWKDLocalPart()', function () {
@ -26,4 +31,89 @@ 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}`)
})
})
})
}) })

8
views/429.pug Normal file
View file

@ -0,0 +1,8 @@
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.

48
views/apps.pug Normal file
View file

@ -0,0 +1,48 @@
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

View file

@ -1,6 +1,6 @@
extends templates/base.pug extends templates/base.pug
block content block content
section.long_form.narrow section
h1= title h1= title
.card !{ content } | !{ content }

View file

@ -1,76 +1,44 @@
extends templates/base.pug extends templates/base.pug
block content block content
#search.form-wrapper.card section#search.form-wrapper
h2#searchTitle View a profile <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>
form(action="post") form(action="post")
label#searchQuery(for="query") Query for fingerprint or email identifier input#query(type="search" name="query" required placeholder="Search for a profile")
input#query(type="search" name="query" required placeholder="3637202523e7c1309ab79e99ef2dc5827b445f4b, test@doip.rocks" aria-labelledby="searchTitle searchQuery") input(type="submit" value="Search")
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++
section
h2 About Keyoxide h2 About Keyoxide
.hcards.hcards--features.hcards--max-2 p Keyoxide is a decentralized tool to create and verify decentralized online identities.
.card 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.
h3 Online identity 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.
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 &amp; 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 p
| Discussion of the Keyoxide project happens primarily on the | 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
a(href="https://community.keyoxide.org") Keyoxide Community Forum a(href="https://community.keyoxide.org") Keyoxide Community Forum
| . This is the place to propose new service providers for identity verification, make feature suggestions or report bugs. | .
| 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.
p p
| There is also the | There is also the
a(href="irc://irc.libera.chat/#keyoxide") #keyoxide:libera.chat IRC room 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 | and the
a(href="https://lists.sr.ht/~yarmo/keyoxide-devel") keyoxide-devel mailing list a(href="https://lists.sr.ht/~yarmo/keyoxide-devel") keyoxide-devel mailing list
| . The IRC room and Matrix channel are bridged together. | . The IRC room and Matrix channel are bridged together.
p p
| The project is also present on the fediverse as | The project is also present on the fediverse:
a(href="https://fosstodon.org/@keyoxide") @keyoxide@fosstodon.org a(href="https://fosstodon.org/@keyoxide") @keyoxide@fosstodon.org
| . | .
p p
@ -79,10 +47,10 @@ block content
| . | .
section
h2 Fund the project h2 Fund the project
.card
p p
| The development of Keyoxide and the Decentralized OpenPGP Identity Proofs ecosystem is entirely funded by donations. | The development of Keyoxide and the Decentralized Online Identity Proofs ecosystem is entirely funded by donations.
p p
| The Keyoxide project was awarded a NGI Zero grant from the | The Keyoxide project was awarded a NGI Zero grant from the
a(href='https://nlnet.nl/') NLnet Foundation a(href='https://nlnet.nl/') NLnet Foundation

View file

@ -1,30 +1,33 @@
footer footer
.container .container
.hcards p
div | This site:
h1 Keyoxide
a(href="/") Homepage a(href="/") Homepage
br | •
a(href="/util") Utilities a(href="/util") Utilities
br | •
a(href="/privacy") Privacy policy a(href="/privacy") Privacy policy
br
div | Keyoxide project:
h1 Keyoxide project a(href="https://community.keyoxide.org") Forum
a(href="https://keyoxide.org") Keyoxide.org | •
br
a(href="https://community.keyoxide.org") Community forum
br
a(href="https://docs.keyoxide.org") Documentation a(href="https://docs.keyoxide.org") Documentation
br | •
a(href="https://blog.keyoxide.org") Blog a(href="https://blog.keyoxide.org") Blog
| •
a(href="https://codeberg.org/keyoxide") Source code
br
div | Related:
h1 Development
a(href="https://codeberg.org/keyoxide/") Source code
br
a(href="https://doip.rocks") doip.rocks a(href="https://doip.rocks") doip.rocks
br | •
a(href="https://ariadne.id") ariadne.id a(href="https://ariadne.id") ariadne.id
p.copyright &copy; 2022 Keyoxide project contributors p
| Version
a(href=meta.keyoxide.sourceUrl)= meta.keyoxide.semver
br
| &copy; 2020-2024 Keyoxide project contributors

View file

@ -1,9 +1,10 @@
header header
.container .container
nav nav
a.logo(href='/' aria-label='Home') a.logo(href='/' aria-label='Home') Keyoxide
img(src='/static/img/logo_circle.png' alt='Keyoxide' aria-hidden='true') .spacer
a.text(href='/') Home .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://docs.keyoxide.org') Docs
a.text(href='https://blog.keyoxide.org') Blog a.text(href='https://blog.keyoxide.org') Blog
a.text(href='https://community.keyoxide.org') Forum

View file

@ -1,42 +1,51 @@
extends templates/base.pug extends templates/base.pug
mixin generatePersona(persona, isPrimary) mixin generatePersona(persona, isPrimary)
if persona.claims.length > 0
h2 h2
if persona.email if persona.email
span.p-email Identity claims (#{persona.email}) | Identity claims (
span.p-email #{persona.email}
| )
else else
span.p-email Identity claims | Identity claims
if isPrimary if isPrimary
small.primary primary small.primary primary
if persona.description if persona.description
span.p-comment &#9432; #{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 each claim in persona.claims
if claim.matches.length > 0 if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim) kx-claim.kx-item(data-claim=claim,data-status='running')
details(aria-label="Claim") details(aria-label="Claim")
summary summary
.info .info
p.subtitle= claim.display.serviceproviderName img(src=`https://design.keyoxide.org/brands/service-providers/_/icon.svg` onerror="this.src='https://design.keyoxide.org/brands/service-providers/_/icon.svg'")
p.title= claim.display.name p
span.title= claim.display.profileName
span.subtitle-wrapper
| [
span.subtitle= claim.display.serviceproviderName
| ]
.icons .icons
.verificationStatus(data-value='running') .verificationStatus
.inProgress .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 .content
.subsection .subsection
img(src='/static/img/link.png') img(src='/static/img/link.png')
div div
if (claim.display.url) p Claim link:
p Profile link: a(rel="me" href=claim.uri aria-label="Link to claim")= claim.uri
a(rel='me' href=claim.display.url aria-label="Link to profile")= claim.display.url
else
p Profile link: not accessible from browser
if (claim.matches.length === 1 && 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 block content
if (data && 'publicKey' in data)
script. script.
kx = { kx = {
publicKey: !{JSON.stringify(data.publicKey)} publicKey: !{JSON.stringify(data.publicKey)}
@ -58,7 +67,7 @@ block content
a(href="https://docs.keyoxide.org/getting-started/something-went-wrong/") documentation a(href="https://docs.keyoxide.org/getting-started/something-went-wrong/") documentation
| for help. | for help.
else else
section.profile.narrow.h-card section.profile
noscript noscript
p Keyoxide requires JavaScript to function. p Keyoxide requires JavaScript to function.
@ -102,10 +111,22 @@ block content
input(type='submit', name='submit', value='Generate profile') input(type='submit', name='submit', value='Generate profile')
unless (isSignature && !signature) unless (isSignature && !signature)
#profileHeader.card.card--transparent.card--profileHeader if (theme)
img#profileAvatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar") 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")
p#profileName.p-name= data.personas[data.primaryPersonaIndex].name p.profile__name.p-name= data.personas[data.primaryPersonaIndex].name
if (data.personas[data.primaryPersonaIndex].description)
p= data.personas[data.primaryPersonaIndex].description
if (enable_message_encryption || enable_signature_verification) if (enable_message_encryption || enable_signature_verification)
.button-wrapper .button-wrapper
@ -114,28 +135,33 @@ block content
if (enable_signature_verification) if (enable_signature_verification)
button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature
.profile__claims
+generatePersona(data.personas[data.primaryPersonaIndex], true && data.personas.length > 1) +generatePersona(data.personas[data.primaryPersonaIndex], true && data.personas.length > 1)
each persona, index in data.personas each persona, index in data.personas
unless index == data.primaryPersonaIndex unless index == data.primaryPersonaIndex
+generatePersona(persona, false) +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
#profileProofs.card.card--transparent section
h2 Key h2 Profile information
if (data && data.publicKey)
h3 Public key
kx-key.kx-item(data-keydata=data.publicKey) kx-key.kx-item(data-keydata=data.publicKey)
details(aria-label="Key") details(aria-label="Key")
summary summary
.info .info
p.subtitle= data.publicKey.fetch.method p
p.title= data.identifier span.title= data.identifier
span.subtitle-wrapper
| [
span.subtitle= data.publicKey.fetch.method
| ]
.content .content
.subsection .subsection
img(src='/static/img/link.png') img(src='/static/img/link.png')
div div
p Key link: p Key link:
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
hr
if (data.profileType === 'openpgp')
.subsection
img(src='/static/img/qrcode.png')
div
button(onClick=`showQR('${data.publicKey.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR

View file

@ -6,6 +6,7 @@ html(lang='en')
meta(name='theme-color' content='#fff') meta(name='theme-color' content='#fff')
meta(name='description' content='Modern and secure platform to manage a decentralized identity based on cryptographic keys') 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='shortcut icon' href='/favicon.svg')
link(rel='stylesheet' href='/static/main.css')
title= (title ? title : 'Keyoxide') title= (title ? title : 'Keyoxide')
include ../partials/header.pug include ../partials/header.pug
@ -16,7 +17,6 @@ html(lang='en')
include ../partials/footer.pug 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/openpgp.js' charset='utf-8')
script(type='application/javascript' defer src='/static/doipFetchers.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/doip.js' charset='utf-8')

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 Argon2 utility h1 Argon2 utility
h2 Generate Argon2 hash h2 Generate Argon2 hash
@ -11,7 +11,7 @@ block content
a(href='https://en.wikipedia.org/wiki/Argon2') Argon2 a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
| hashes useful to | hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid proof! | . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
h3 Input h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input) input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash h3 Hash

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 bcrypt utility h1 bcrypt utility
h2 Generate bcrypt hash h2 Generate bcrypt hash
@ -11,7 +11,7 @@ block content
a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt
| hashes useful to | hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid proof! | . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
h3 Input h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input) input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash h3 Hash

View file

@ -1,8 +1,8 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 Keyoxide utilities h2 Keyoxide utilities
p p
a(href="/util/profile-url") Get the URL for a Keyoxide profile a(href="/util/profile-url") Get the URL for a Keyoxide profile
p p

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 Profile URL h1 Profile URL
form#form-util-profile-url(method='post') form#form-util-profile-url(method='post')
p This tool generates an URL for your Keyoxide profile page. p This tool generates an URL for your Keyoxide profile page.

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 QR Code h1 QR Code
form#form-util-qr(method='post') form#form-util-qr(method='post')
pre pre

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 QR Code h1 QR Code
form#form-util-qrfp(method='post') form#form-util-qrfp(method='post')
p p

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug extends ../templates/base.pug
block content block content
section.narrow section
h1 Web Key Directory URL generator h1 Web Key Directory URL generator
form#form-util-wkd(method='post') form#form-util-wkd(method='post')
p p

View file

@ -25,10 +25,11 @@ export default (env) => {
module: { module: {
rules: [ rules: [
{ {
test: /\.css$/, test: /\.s[ca]ss$/,
use: [ use: [
MiniCssExtractPlugin.loader, MiniCssExtractPlugin.loader,
'css-loader' 'css-loader',
'sass-loader'
] ]
} }
] ]

6904
yarn.lock

File diff suppressed because it is too large Load diff