Compare commits

..

No commits in common. "dev" and "support-aspe" have entirely different histories.

116 changed files with 51754 additions and 100536 deletions

View file

@ -4,10 +4,7 @@
"es2021": true,
"node": true
},
"extends": [
"standard",
"plugin:jsdoc/recommended"
],
"extends": "standard",
"overrides": [
],
"parserOptions": {
@ -15,8 +12,5 @@
"sourceType": "module"
},
"rules": {
},
"plugins": [
"jsdoc"
]
}
}

View file

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

View file

@ -1,22 +0,0 @@
---
name: 'Claim verification bug'
about: 'Report a claim no longer verifying, or not verifying as it should'
title: '[CLAIM BUG] '
ref: 'dev'
labels:
- 'Status/Needs Triage'
- Type/Bug
---
### Service provider
Name:
### Profile with the bug
<!-- Optional: only if you're willing to share your profile -->
Link to profile:
### What happened

View file

@ -1,38 +0,0 @@
---
name: 'New claim'
about: 'Suggest a new service provider or website for identity verification'
title: '[NEW CLAIM] '
ref: 'dev'
labels:
- 'Status/Needs Triage'
- 'Type/New Claim'
---
### Service provider
Name:
Short description:
Website:
API documentation:
### Proposed verification mechanism
<!-- Optional, only fill in if you already know which APIs to use, etc -->
### Remarks
### Tasks
<!-- Leave the following unchecked -->
- [ ] Verification mechanism tested
- [ ] Added to [doip-js](https://codeberg.org/keyoxide/doipjs)
- [ ] Added to [doip-rs](https://codeberg.org/keyoxide/doip-rs)
- [ ] Added proxy routes (if needed)
- [ ] Added to [keyoxide-brands](https://codeberg.org/keyoxide/keyoxide-brands)
- [ ] Added to [documentation](https://codeberg.org/keyoxide/keyoxide-docs)

View file

@ -16,7 +16,6 @@ examples
package.json
yarn.lock
rollup.config.js
\.editorconfig
\.gitignore
\.licenseignore

View file

@ -1,8 +1,8 @@
when:
pipeline:
prepare:
when:
branch: main
event: tag
steps:
prepare:
image: node
commands:
- yarn --pure-lockfile

View file

@ -1,4 +1,4 @@
steps:
pipeline:
test:
image: node
commands:

Binary file not shown.

View file

@ -1,6 +0,0 @@
nodeLinker: node-modules
npmScopes:
myriation:
npmPublishRegistry: https://git.myriation.xyz/api/packages/myriation/npm/
npmAlwaysAuth: true
npmAuthToken: REPLACE-ME

View file

@ -5,105 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.9] - 2024-02-01
### Added
- ORCiD identity claims
### Changed
- Improved code documentation
- Optimized creation of Regexp instances
### Fixed
- Bad promise timeout logic
- Dependencies cleaned up
## [1.2.8] - 2024-01-23
### Added
- OpenPGP and ASP claims
## [1.2.7] - 2023-10-09
### Fixed
- Fix regex errors
## [1.2.6] - 2023-10-09
### Added
- JSON schemas for common objects
### Changed
- Additional Github proof location (proof.md)
### Fixed
- IRC compatibility with ASP profiles
- IRC profile display value
- Lobste.rs profile URL value
## [1.2.5] - 2023-10-05
### Added
- Support for theme color
## [1.2.4] - 2023-10-04
### Changed
- Claim display information
## [1.2.3] - 2023-10-03
### Fixed
- Claim ambiguity logic
## [1.2.2] - 2023-10-03
### Fixed
- Service provider information for Lichess and Keybase
- Display data logic in claim toJSON
## [1.2.1] - 2023-09-23
Bump necessary due to tag-related glitch in git forge
## [1.2.0] - 2023-09-23
### Added
- Allow service providers to validate the claim verification result (useful for forks)
- Support for Forgejo claims
## [1.1.1] - 2023-09-22
### Fixed
- Normalize case before hashed proof verification
## [1.1.0] - 2023-09-21
### Changed
- Unify fromJSON() for Profile, Persona and Claim classes
## [1.0.4] - 2023-09-19
### Fixed
- Allow the activitypub Person request to fail
## [1.0.3] - 2023-09-19
### Fixed
- Avoid using potentially missing URL for ActivityPub postprocessing
## [1.0.2] - 2023-09-19
### Fixed
- Make nodeinfo requests use HTTPS
## [1.0.1] - 2023-09-18
### Fixed
- Ignore OpenPGP users without userId
- OpenCollective GraphQL queries
- Improve ActivityPub post proofs support
## [1.0.0] - 2023-07-13
### Changed
- Moved from CommonJS to ESM
- All profiles now use the Profile class
- Functions that used to return OpenPGP keys now return Profile objects
- Compliance with https://spec.keyoxide.org/spec/2/
## [0.19.0] - 2023-07-04
### Added
- Support for ASPE protocol
### Changed
- Made HTTP scheme for proxy calls configurable
- Replaced standard with eslint
### Fixed
- Sort OpenPGP certifications by chronological order
- Allowing white space in fingerprint
- Use correct format for displaying ActivityPub claims
- Missing JSDOC types
- JS bundling
## [0.18.3] - 2023-03-27
### Added

View file

@ -1,18 +1,22 @@
# doip.js
[![status-badge](https://ci.codeberg.org/api/badges/5907/status.svg)](https://ci.codeberg.org/repos/5907)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue?style=flat)](https://codeberg.org/keyoxide/doipjs/src/branch/main/LICENSE)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=flat)](https://fosstodon.org/@keyoxide)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
![](static/doip.png)
![](doip.png)
[doip.js](https://codeberg.org/keyoxide/doipjs) allows websites and Node.js projects to verify decentralized online
identities.
doip.js allows websites and Node.js projects to verify decentralized online
identities based on OpenPGP.
Source code available at [codeberg.org](https://codeberg.org/keyoxide/doipjs).
Documentation available at [js.doip.rocks](https://js.doip.rocks).
## Features
- Verify online identities using decentralized technology
- Based on [OpenPGP](https://www.openpgp.org), a widely-used cryptographic standard
- Regex-based service provider detection
- [Mocha](https://mochajs.org) tests
## Installation (node)
Install using **yarn** or **npm**:
@ -52,32 +56,32 @@ const verifyIdentity = async (url, fp) => {
verifyIdentity('dns:doip.rocks', '9f0048ac0b23301e1f77e994909f6bd6f80f485d')
```
This snippet verifies the [doip.rocks](https://doip.rocks) domain as
This snippet works and will verify the [doip.rocks](https://doip.rocks) domain as
bidirectionally linked to Yarmo's cryptographic key.
## Contributing
## About Keyoxide
Anyone can contribute!
[Keyoxide](https://keyoxide.org/), made by Yarmo Mackenbach, is a modern, secure
and privacy-friendly platform to establish decentralized online identities using
a novel concept know as [DOIP](doip.md). In an effort to make this technology
accessible for other projects and stimulate the emergence of both complementary
and competing projects, this project-agnostic library is
[published on codeberg.org](https://codeberg.org/keyoxide/doipjs) and open
sourced under the
[Apache-2.0](https://codeberg.org/keyoxide/doipjs/src/branch/main/LICENSE)
license.
Developers are invited to:
## Community
- fork the repository and play around
- submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/doipjs/issues)
There's a [Keyoxide Matrix room](https://matrix.to/#/#keyoxide:matrix.org) where
we discuss everything DOIP and Keyoxide.
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/doipjs/issues?q=&type=all&state=open&labels=183598) that you could look into.
## Donate
Everyone is invited to:
Please consider [donating](https://liberapay.com/Keyoxide/) if you think this
project is a step in the right direction for the internet.
- find and [report bugs](https://codeberg.org/keyoxide/doipjs/issues/new/choose)
- suggesting [new features](https://codeberg.org/keyoxide/doipjs/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
## Funding
Please note that this project has a [Code of Conduct](https://codeberg.org/keyoxide/web/src/branch/main/CODE_OF_CONDUCT.md) that all contributors agree to abide when participating.
## About the Keyoxide project
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!
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.
This library was realized with funding from
[NLnet](https://nlnet.nl/project/Keyoxide/).

14597
dist/doip.core.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

36961
dist/doip.fetchers.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

43481
dist/doip.js vendored Normal file

File diff suppressed because one or more lines are too long

32
dist/doip.min.js vendored Normal file

File diff suppressed because one or more lines are too long

23
examples/fetch-key-hkp.js Normal file
View file

@ -0,0 +1,23 @@
const doip = require('../src')
const main = async () => {
// Fetch the key using HKP
const key = await doip.keys.fetchHKP("test@doip.rocks")
// Process it to extract the UIDs and their claims
const obj = await doip.keys.process(key)
// Process every claim for every user
obj.users.forEach(async user => {
user.claims.forEach(async claim => {
// Match the claim
await claim.match()
// Verify the claim
await claim.verify()
console.log(claim)
})
})
}
main()

View file

@ -1,4 +1,4 @@
import * as doip from '../src/index.js'
const doip = require('../src')
const main = async () => {
// Obtain the plaintext public key
@ -28,11 +28,14 @@ fCRSXrr7SZxIu7I8jfQrxc0k9XhpPI/gdlgRqoEG2lMyqFaWzyoI9dyoVwji78rg
=Csr+
-----END PGP PUBLIC KEY BLOCK-----`
// Use the plaintext key to get a profile
const profile = await doip.openpgp.fetchPlaintext(pubKeyPlaintext)
// Fetch the key using WKD
const key = await doip.keys.fetchPlaintext(pubKeyPlaintext)
// Process it to extract the UIDs and their claims
const obj = await doip.keys.process(key)
// Log the claims of the first UID
console.log(profile.personas[0].claims)
console.log(obj.users[0].claims)
}
main()

14
examples/fetch-key-wkd.js Normal file
View file

@ -0,0 +1,14 @@
const doip = require('../src')
const main = async () => {
// Fetch the key using WKD
const key = await doip.keys.fetchWKD("test@doip.rocks")
// Process it to extract the UIDs and their claims
const obj = await doip.keys.process(key)
// Log the claims of the first UID
console.log(obj.users[0].claims)
}
main()

View file

@ -1,8 +1,9 @@
import * as doip from '../src/index.js'
const doip = require('../src')
const main = async () => {
// Fetch the profile using ASPE
const profile = await doip.asp.fetchASPE("aspe:keyoxide.org:6WJK26YKF6WUVPIZTS2I2BIT64")
console.log(profile);
// Process every claim for every persona
profile.personas[0].claims.forEach(async claim => {

View file

@ -1,20 +0,0 @@
import * as doip from '../src/index.js'
const main = async () => {
// Fetch the profile using HKP
const profile = await doip.openpgp.fetchHKP("test@doip.rocks")
// Process every claim for every persona
profile.personas.forEach(async persona => {
persona.claims.forEach(async claim => {
// Match the claim
await claim.match()
// Verify the claim
await claim.verify()
console.log(claim)
})
})
}
main()

View file

@ -1,11 +0,0 @@
import * as doip from '../src/index.js'
const main = async () => {
// Fetch the profile using WKD
const profile = await doip.openpgp.fetchWKD("test@doip.rocks")
// Log the claims of the first persona
console.log(profile.personas[0].claims)
}
main()

View file

@ -1,4 +1,4 @@
import * as doip from '../src/index.js'
const doip = require('../src')
const signature = `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
@ -25,10 +25,10 @@ cXbjvHSGniZ7M3S9S8knAfIquPvTp7+L7wWgSSB5VObPp1r+96n87hyFZUp7PCvl
const main = async () => {
// Process the OpenPGP signature
const profile = await doip.signatures.process(signature)
const sigProfile = await doip.signatures.process(signature)
// Log the claims of the first persona
console.log(profile.users[0].claims)
// Log the processed signature profile
console.log(sigProfile.users[0].claims)
}
main()

View file

@ -1,9 +0,0 @@
import * as doip from '../src/index.js'
const main = async () => {
// const sp = doip.ServiceProviderDefinitions.data['activitypub'].processURI('https://fosstodon.org/@yarmo')
const sp = doip.ServiceProviderDefinitions.data['discourse'].processURI('https://domain.org/u/alice')
console.log(sp);
}
main()

View file

@ -1,4 +1,4 @@
import * as doip from '../src/index.js'
const doip = require('../src')
const main = async () => {
// Generate the claim

View file

@ -1,8 +1,5 @@
{
"plugins": [
"plugins/markdown",
"node_modules/jsdoc-tsimport-plugin"
],
"plugins": ["plugins/markdown"],
"source": {
"include": ["./src", "./README.md"]
},
@ -15,26 +12,21 @@
}
},
"opts": {
"template": "node_modules/docdash",
"destination": "docs/"
"template": "node_modules/clean-jsdoc-theme",
"theme_opts": {
"theme": "light",
"menu": [
{
"title": "Source code",
"link": "https://codeberg.org/keyoxide/doipjs",
"target": "_blank"
},
"docdash": {
"collapse": true,
"meta": {
"title": "doipjs",
"description": "Documentation for the doip.js library"
},
"menu": {
"Keyoxide": {
"href":"https://keyoxide.org",
"target":"_blank",
"class":"menu-item"
},
"Keyoxide docs": {
"href":"https://docs.keyoxide.org",
"target":"_blank",
"class":"menu-item"
{
"title": "Keyoxide",
"link": "https://keyoxide.org",
"target": "_blank"
}
]
}
}
}

View file

@ -1,79 +1,68 @@
{
"name": "@myriation/doipjs",
"version": "1.2.9+myriaiton.1",
"name": "doipjs",
"version": "0.18.3",
"description": "Decentralized Online Identity Proofs library in Node.js",
"type": "module",
"main": "./src/index.js",
"exports": {
".": {
"default": "./src/index.js"
},
"./fetchers": {
"default": "./src/fetcher/index.js"
},
"./fetchers-minimal": {
"default": "./src/fetcher/index.minimal.js"
}
},
"packageManager": "yarn@4.3.0",
"packageManager": "yarn@1.22.19",
"dependencies": {
"@openpgp/hkp-client": "^0.0.3",
"@openpgp/wkd-client": "^0.0.4",
"@xmpp/client": "^0.13.1",
"@xmpp/debug": "^0.13.0",
"axios": "^1.6.5",
"axios": "^0.25.0",
"browser-or-node": "^1.3.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"entities": "^4.4.0",
"express": "^4.17.1",
"express-validator": "^6.10.0",
"hash-wasm": "^4.9.0",
"irc-upd": "^0.11.0",
"jose": "^4.14.4",
"merge-options": "^3.0.3",
"openpgp": "^5.5.0",
"rfc4648": "^1.5.2",
"rome": "^11.0.0",
"valid-url": "^1.0.9",
"validator": "^13.9.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"browserify": "^17.0.0",
"browserify-shim": "^3.8.14",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"docdash": "^2.0.2",
"chai-match-pattern": "^1.2.0",
"clean-jsdoc-theme": "^3.2.4",
"eslint": "^8.39.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsdoc": "^48.0.4",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"husky": "^7.0.0",
"jsdoc": "^4.0.2",
"jsdoc-tsimport-plugin": "^1.0.5",
"jsdoc": "^3.6.6",
"license-check-and-add": "^4.0.3",
"lint-staged": "^11.0.0",
"minify": "^9.1",
"mocha": "^9.2.0",
"rollup": "^3.26.2",
"rollup-plugin-polyfill-node": "^0.12.0",
"rollup-plugin-visualizer": "^5.9.2"
"nodemon": "^2.0.19"
},
"scripts": {
"release": "node ./prerelease.js && yarn run test && yarn run build",
"build": "rm -rf ./dist/ && yarn run build:bundle && yarn run build:minify",
"build:bundle": "rollup -c",
"build:minify": "minify ./dist/doip.core.js > ./dist/doip.core.min.js && minify ./dist/doip.fetchers.js > ./dist/doip.fetchers.min.js && minify ./dist/doip.fetchers.minimal.js > ./dist/doip.fetchers.minimal.min.js",
"license:check": "license-check-and-add check",
"license:add": "license-check-and-add add",
"license:remove": "license-check-and-add remove",
"docs:lib": "jsdoc -c jsdoc-lib.json -r -d ./docs -P package.json",
"release": "yarn run test && yarn run release:bundle && yarn run release:minify",
"release:bundle": "./node_modules/.bin/browserify ./src/index.js --standalone doip -x openpgp -x @xmpp/client -x @xmpp/debug -x irc-upd -o ./dist/doip.js",
"release:minify": "./node_modules/.bin/minify ./dist/doip.js > ./dist/doip.min.js",
"license:check": "./node_modules/.bin/license-check-and-add check",
"license:add": "./node_modules/.bin/license-check-and-add add",
"license:remove": "./node_modules/.bin/license-check-and-add remove",
"docs:lib": "./node_modules/.bin/jsdoc -c jsdoc-lib.json -r -d ./docs -P package.json",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"test": "yarn lint && yarn run license:check && yarn run mocha",
"mocha": "./node_modules/.bin/mocha",
"test": "yarn lint && yarn rome check ./src/* && yarn run license:check && yarn run mocha",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "https://git.myriation.org/myriation/doipjs"
"url": "https://codeberg.org/keyoxide/doipjs"
},
"homepage": "https://js.doip.rocks",
"keywords": [
@ -85,5 +74,13 @@
"identity"
],
"author": "Yarmo Mackenbach <yarmo@yarmo.eu> (https://yarmo.eu)",
"license": "Apache-2.0"
"license": "Apache-2.0",
"browserify": {
"transform": [
"browserify-shim"
]
},
"browserify-shim": {
"openpgp": "global:openpgp"
}
}

View file

@ -1,33 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as C from './src/constants.js'
import { readFile } from 'fs/promises'
const main = async () => {
const pkg = JSON.parse(
await readFile(
new URL('./package.json', import.meta.url)
)
)
// Assert that the constant version equals the package version
if (C.version !== pkg.version) {
console.log(`!!! Mismatch between constants.js version (${C.version}) and package.json version (${pkg.version})`)
process.exit(1)
}
}
main()

View file

@ -1,73 +0,0 @@
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import nodePolyfills from 'rollup-plugin-polyfill-node'
import { fileURLToPath } from 'node:url'
const fetchersExternalId = fileURLToPath(
new URL(
'src/fetcher/index.js',
import.meta.url
)
)
export default [
{
input: 'src/index.js',
output: {
file: './dist/doip.core.js',
format: 'iife',
name: 'doip',
globals: {
'openpgp': 'openpgp',
[fetchersExternalId]: 'doipFetchers'
},
},
watch: {
include: './src/**'
},
external: ['openpgp', './fetcher/index.js'],
plugins: [
commonjs(),
json(),
nodeResolve({
browser: true
}),
nodePolyfills()
]
},
{
input: 'src/fetcher/index.js',
output: {
file: './dist/doip.fetchers.js',
format: 'iife',
name: 'doipFetchers'
},
external: [],
plugins: [
commonjs(),
json(),
nodeResolve({
browser: true
}),
nodePolyfills()
]
},
{
input: 'src/fetcher/index.minimal.js',
output: {
file: './dist/doip.fetchers.minimal.js',
format: 'iife',
name: 'doipFetchers'
},
external: [],
plugins: [
commonjs(),
json(),
nodeResolve({
browser: true
}),
nodePolyfills()
]
}
]

8
rome.json Normal file
View file

@ -0,0 +1,8 @@
{
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

View file

@ -13,13 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import axios from 'axios'
import { decodeProtectedHeader, importJWK, compactVerify, calculateJwkThumbprint } from 'jose'
import { base32, base64url } from 'rfc4648'
import { Claim } from './claim.js'
import { Persona } from './persona.js'
import { Profile } from './profile.js'
import { ProfileType, PublicKeyEncoding, PublicKeyFetchMethod, PublicKeyType } from './enums.js'
const axios = require('axios').default
const jose = require('jose')
const { base32, base64url } = require('rfc4648')
const Claim = require('./claim')
const Persona = require('./persona')
const Profile = require('./profile')
const SupportedCryptoAlg = ['EdDSA', 'ES256', 'ES256K', 'ES384', 'ES512']
@ -32,11 +31,11 @@ const SupportedCryptoAlg = ['EdDSA', 'ES256', 'ES256K', 'ES384', 'ES512']
* Fetch a public key using Web Key Directory
* @function
* @param {string} uri - ASPE URI
* @returns {Promise<Profile>} The fetched profile
* @returns {Promise<Profile>}
* @example
* const key = await doip.aspe.fetchASPE('aspe:domain.example:1234567890');
* const key = doip.aspe.fetchASPE('aspe:domain.tld:1234567890');
*/
export async function fetchASPE (uri) {
const fetchASPE = async uri => {
const re = /aspe:(.*):(.*)/
if (!re.test(uri)) {
@ -67,29 +66,24 @@ export async function fetchASPE (uri) {
throw new Error(`Error fetching Keybase key: ${e.message}`)
}
const profile = await parseProfileJws(profileJws, uri)
profile.publicKey.fetch.method = PublicKeyFetchMethod.ASPE
profile.publicKey.fetch.query = uri
profile.publicKey.fetch.resolvedUrl = profileUrl
return profile
return await parseProfileJws(profileJws, uri)
}
/**
* Parse a JWS and extract the profile it contains
* Fetch a public key using Web Key Directory
* @function
* @param {string} profileJws - Compact-Serialized profile JWS
* @param {string} uri - The ASPE URI associated with the profile
* @returns {Promise<Profile>} The extracted profile
* @returns {Promise<Profile>}
* @example
* const key = await doip.aspe.parseProfileJws('...', 'aspe:domain.example:123');
* const key = doip.aspe.parseProfileJws('...');
*/
export async function parseProfileJws (profileJws, uri) {
const parseProfileJws = async (profileJws, uri) => {
const matches = uri.match(/aspe:(.*):(.*)/)
const localPart = matches[2].toUpperCase()
// Decode the headers
const protectedHeader = decodeProtectedHeader(profileJws)
const protectedHeader = jose.decodeProtectedHeader(profileJws)
// Extract the JWK
if (!SupportedCryptoAlg.includes(protectedHeader.alg)) {
@ -101,7 +95,7 @@ export async function parseProfileJws (profileJws, uri) {
if (!protectedHeader.jwk) {
throw new Error('Invalid profile JWS: missing key')
}
const publicKey = await importJWK(protectedHeader.jwk, protectedHeader.alg)
const publicKey = await jose.importJWK(protectedHeader.jwk, protectedHeader.alg)
// Compute and verify the fingerprint
const fp = await computeJwkFingerprint(protectedHeader.jwk)
@ -114,7 +108,7 @@ export async function parseProfileJws (profileJws, uri) {
}
// Decode the payload
const { payload } = await compactVerify(profileJws, publicKey)
const { payload } = await jose.compactVerify(profileJws, publicKey)
const payloadJson = JSON.parse(new TextDecoder().decode(payload))
// Verify the payload
@ -130,54 +124,31 @@ export async function parseProfileJws (profileJws, uri) {
const profileName = payloadJson['http://ariadne.id/name']
/** @type {string} */
const profileDescription = payloadJson['http://ariadne.id/description']
/** @type {string} */
const profileThemeColor = payloadJson['http://ariadne.id/color']
/** @type {Array<string>} */
/** @type {string[]} */
const profileClaims = payloadJson['http://ariadne.id/claims']
const profileClaimsParsed = profileClaims.map(x => new Claim(x, uri))
const pe = new Persona(profileName, profileClaimsParsed)
if (profileDescription) {
pe.setDescription(profileDescription)
}
if (profileThemeColor && /^#([0-9A-F]{3}){1,2}$/i.test(profileThemeColor)) {
pe.themeColor = profileThemeColor
}
const pe = new Persona(profileName, profileDescription || '', profileClaimsParsed)
const pr = new Profile([pe])
pr.primaryPersona = 0
const profile = new Profile(ProfileType.ASP, uri, [pe])
profile.publicKey.fingerprint = fp
profile.publicKey.encoding = PublicKeyEncoding.JWK
profile.publicKey.encodedKey = JSON.stringify(protectedHeader.jwk)
profile.publicKey.key = protectedHeader.jwk
switch (protectedHeader.alg) {
case 'ES256':
profile.publicKey.keyType = PublicKeyType.ES256
break
case 'EdDSA':
profile.publicKey.keyType = PublicKeyType.EDDSA
break
default:
profile.publicKey.keyType = PublicKeyType.UNKNOWN
break
}
return profile
return pr
}
/**
* Compute the fingerprint for {@link https://github.com/panva/jose/blob/main/docs/interfaces/types.JWK.md JWK} keys
* Compute the fingerprint for JWK keys
* @function
* @param {import('jose').JWK} key - The JWK public key for which to compute the fingerprint
* @returns {Promise<string>} The computed fingerprint
* @param {jose.JWK} key
* @returns {Promise<string>}
*/
export async function computeJwkFingerprint (key) {
const thumbprint = await calculateJwkThumbprint(key, 'sha512')
const computeJwkFingerprint = async key => {
const thumbprint = await jose.calculateJwkThumbprint(key, 'sha512')
const fingerprintBytes = base64url.parse(thumbprint, { loose: true }).slice(0, 16)
const fingerprint = base32.stringify(fingerprintBytes, { pad: false })
return fingerprint
}
exports.fetchASPE = fetchASPE
exports.parseProfileJws = parseProfileJws

View file

@ -13,105 +13,74 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import isAlphanumeric from 'validator/lib/isAlphanumeric.js'
import { isUri } from 'valid-url'
import mergeOptions from 'merge-options'
import { fetch } from './proofs.js'
import { run } from './verifications.js'
import { list, data as _data } from './serviceProviders/index.js'
import { opts as _opts } from './defaults.js'
import { ClaimStatus } from './enums.js'
import { ServiceProvider } from './serviceProvider.js'
const validator = require('validator').default
const validUrl = require('valid-url')
const mergeOptions = require('merge-options')
const proofs = require('./proofs')
const verifications = require('./verifications')
const claimDefinitions = require('./claimDefinitions')
const defaults = require('./defaults')
const E = require('./enums')
/**
* @class
* @classdesc Identity claim
* @classdesc OpenPGP-based identity claim
* @property {string} uri - The claim's URI
* @property {string} fingerprint - The fingerprint to verify the claim against
* @property {number} status - The current status code of the claim
* @property {string} status - The current status of the claim
* @property {Array<object>} matches - The claim definitions matched against the URI
* @property {object} verification - The result of the verification process
*/
class Claim {
/**
* Initialize a Claim object
* @constructor
* @param {string|object} [uri] - The URI of the identity claim or a JSONified Claim instance
* @param {string} [fingerprint] - The fingerprint of the OpenPGP key
* @example
* const claim = doip.Claim();
* const claim = doip.Claim('dns:domain.tld?type=TXT');
* const claim = doip.Claim('dns:domain.tld?type=TXT', '123abc123abc');
*/
export class Claim {
/**
* Initialize a Claim object
* @param {string} [uri] - The URI of the identity claim
* @param {string} [fingerprint] - The fingerprint of the OpenPGP key
* const claimAlt = doip.Claim(JSON.stringify(claim));
*/
constructor (uri, fingerprint) {
// Import JSON
if (typeof uri === 'object' && 'claimVersion' in uri) {
const data = uri
switch (data.claimVersion) {
case 1:
this._uri = data.uri
this._fingerprint = data.fingerprint
this._status = data.status
this._matches = data.matches
this._verification = data.verification
break
default:
throw new Error('Invalid claim version')
}
return
}
// Verify validity of URI
if (uri && !isUri(uri)) {
if (uri && !validUrl.isUri(uri)) {
throw new Error('Invalid URI')
}
// Verify validity of fingerprint
if (fingerprint) {
try {
// @ts-ignore
isAlphanumeric.default(fingerprint)
validator.isAlphanumeric(fingerprint)
} catch (err) {
throw new Error('Invalid fingerprint')
}
}
/**
* @type {string}
*/
this._uri = uri || ''
/**
* @type {string}
*/
this._fingerprint = fingerprint || ''
/**
* @type {number}
*/
this._status = ClaimStatus.INIT
/**
* @type {Array<ServiceProvider>}
*/
this._status = E.ClaimStatus.INIT
this._matches = []
}
/**
* @function
* @param {*} claimObject - JSON representation of a claim
* @returns {Claim} Parsed claim
* @throws Will throw an error if the JSON object can't be coerced into a Claim
* @example
* doip.Claim.fromJSON(JSON.stringify(claim));
*/
static fromJSON (claimObject) {
/** @type {Claim} */
let claim
let result
if (typeof claimObject === 'object' && 'claimVersion' in claimObject) {
switch (claimObject.claimVersion) {
case 1:
result = importJsonClaimVersion1(claimObject)
if (result instanceof Error) {
throw result
}
claim = result
break
case 2:
result = importJsonClaimVersion2(claimObject)
if (result instanceof Error) {
throw result
}
claim = result
break
default:
throw new Error('Invalid claim version')
}
}
return claim
this._verification = {}
}
get uri () {
@ -127,20 +96,27 @@ export class Claim {
}
get matches () {
if (this._status === ClaimStatus.INIT) {
if (this._status === E.ClaimStatus.INIT) {
throw new Error('This claim has not yet been matched')
}
return this._matches
}
get verification () {
if (this._status !== E.ClaimStatus.VERIFIED) {
throw new Error('This claim has not yet been verified')
}
return this._verification
}
set uri (uri) {
if (this._status !== ClaimStatus.INIT) {
if (this._status !== E.ClaimStatus.INIT) {
throw new Error(
'Cannot change the URI, this claim has already been matched'
)
}
// Verify validity of URI
if (uri.length > 0 && !isUri(uri)) {
if (uri.length > 0 && !validUrl.isUri(uri)) {
throw new Error('The URI was invalid')
}
// Remove leading and trailing spaces
@ -150,7 +126,7 @@ export class Claim {
}
set fingerprint (fingerprint) {
if (this._status === ClaimStatus.VERIFIED) {
if (this._status === E.ClaimStatus.VERIFIED) {
throw new Error(
'Cannot change the fingerprint, this claim has already been verified'
)
@ -166,22 +142,26 @@ export class Claim {
throw new Error("Cannot change a claim's matches")
}
set verification (anything) {
throw new Error("Cannot change a claim's verification result")
}
/**
* Match the claim's URI to candidate definitions
* @function
*/
match () {
if (this._status !== ClaimStatus.INIT) {
if (this._status !== E.ClaimStatus.INIT) {
throw new Error('This claim was already matched')
}
if (this._uri.length === 0 || !isUri(this._uri)) {
if (this._uri.length === 0 || !validUrl.isUri(this._uri)) {
throw new Error('This claim has no URI')
}
this._matches = []
list.every((name, i) => {
const def = _data[name]
claimDefinitions.list.every((name, i) => {
const def = claimDefinitions.data[name]
// If the candidate is invalid, continue matching
if (!def.reURI.test(this._uri)) {
@ -194,7 +174,7 @@ export class Claim {
return true
}
if (candidate.claim.uriIsAmbiguous) {
if (candidate.match.isAmbiguous) {
// Add to the possible candidates
this._matches.push(candidate)
} else {
@ -207,7 +187,7 @@ export class Claim {
return true
})
this._status = this._matches.length === 0 ? ClaimStatus.NO_MATCHES : ClaimStatus.MATCHED
this._status = E.ClaimStatus.MATCHED
}
/**
@ -215,14 +195,15 @@ export class Claim {
* checked for the fingerprint. The verification stops when either a positive
* result was obtained, or an unambiguous claim definition was processed
* regardless of the result.
* @async
* @function
* @param {import('./types').VerificationConfig} [opts] - Options for proxy, fetchers
* @param {object} [opts] - Options for proxy, fetchers
*/
async verify (opts) {
if (this._status === ClaimStatus.INIT) {
if (this._status === E.ClaimStatus.INIT) {
throw new Error('This claim has not yet been matched')
}
if (this._status >= 200) {
if (this._status === E.ClaimStatus.VERIFIED) {
throw new Error('This claim has already been verified')
}
if (this._fingerprint.length === 0) {
@ -230,34 +211,35 @@ export class Claim {
}
// Handle options
opts = mergeOptions(_opts, opts || {})
opts = mergeOptions(defaults.opts, opts || {})
// If there are no matches
if (this._matches.length === 0) {
this.status = ClaimStatus.NO_MATCHES
this._verification = {
result: false,
completed: true,
proof: {},
errors: ['No matches for claim']
}
}
// For each match
for (let index = 0; index < this._matches.length; index++) {
// Continue if a result was already obtained
if (this._status >= 200) { continue }
let claimData = this._matches[index]
/** @type {import('./types').VerificationResult | null} */
let verificationResult = null
let proofData = null
let proofFetchError
try {
proofData = await fetch(claimData, opts)
proofData = await proofs.fetch(claimData, opts)
} catch (err) {
proofFetchError = err
}
if (proofData) {
// Run the verification process
verificationResult = await run(
verificationResult = await verifications.run(
proofData.result,
claimData,
this._fingerprint
@ -267,18 +249,11 @@ export class Claim {
viaProxy: proofData.viaProxy
}
// Validate the result
const def = _data[claimData.about.id]
if (def.functions?.validate && verificationResult.completed && verificationResult.result) {
try {
(verificationResult.result = await def.functions.validate(claimData, proofData, verificationResult, opts))
} catch (_) {}
}
// Post process the data
const def = claimDefinitions.data[claimData.serviceprovider.name]
if (def.functions?.postprocess) {
try {
({ claimData, proofData } = await def.functions.postprocess(claimData, proofData, opts))
({ claimData, proofData } = def.functions.postprocess(claimData, proofData))
} catch (_) {}
}
} else {
@ -286,7 +261,7 @@ export class Claim {
verificationResult = verificationResult || {
result: false,
completed: true,
proof: null,
proof: {},
errors: [proofFetchError]
}
}
@ -296,13 +271,25 @@ export class Claim {
continue
}
if (verificationResult.result) {
this._status = verificationResult.proof.viaProxy ? ClaimStatus.VERIFIED_VIA_PROXY : ClaimStatus.VERIFIED
if (verificationResult.completed) {
// Store the result, keep a single match and stop verifying
this._verification = verificationResult
this._matches = [claimData]
index = this._matches.length
}
}
this._status = this._status >= 200 ? this._status : ClaimStatus.NO_PROOF_FOUND
// Fail safe verification result
this._verification = Object.keys(this._verification).length > 0
? this._verification
: {
result: false,
completed: true,
proof: {},
errors: []
}
this._status = E.ClaimStatus.VERIFIED
}
/**
@ -310,115 +297,34 @@ export class Claim {
* of the candidates is unambiguous. An ambiguous claim should never be
* displayed in an user interface when its result is negative.
* @function
* @returns {boolean} Whether the claim is ambiguous
* @returns {boolean}
*/
isAmbiguous () {
if (this._status < ClaimStatus.MATCHED) {
if (this._status === E.ClaimStatus.INIT) {
throw new Error('The claim has not been matched yet')
}
if (this._matches.length === 0) {
throw new Error('The claim has no matches')
}
if (this._status >= 200 && this._status < 300) return false
return this._matches.length > 1 || this._matches[0].claim.uriIsAmbiguous
return this._matches.length > 1 || this._matches[0].match.isAmbiguous
}
/**
* Get a JSON representation of the Claim object. Useful when transferring
* data between instances/machines.
* @function
* @returns {object} JSON reprentation of the claim
* @returns {object}
*/
toJSON () {
let displayProfileName = this._uri
let displayProfileUrl = null
let displayProofUrl = null
let displayServiceProviderName = null
let displayServiceProviderId = null
if (this._status >= ClaimStatus.MATCHED && this._matches.length > 0 && !this.isAmbiguous()) {
displayProfileName = this._matches[0].profile.display
displayProfileUrl = this._matches[0].profile.uri
displayProofUrl = this._matches[0].proof.request.uri
displayServiceProviderName = this._matches[0].about.name
displayServiceProviderId = this._matches[0].about.id
}
return {
claimVersion: 2,
claimVersion: 1,
uri: this._uri,
proofs: [this._fingerprint],
matches: this._matches.map(x => x.toJSON()),
fingerprint: this._fingerprint,
status: this._status,
display: {
profileName: displayProfileName,
profileUrl: displayProfileUrl,
proofUrl: displayProofUrl,
serviceProviderName: displayServiceProviderName,
serviceProviderId: displayServiceProviderId
}
matches: this._matches,
verification: this._verification
}
}
}
/**
* @ignore
* @param {object} claimObject - JSON representation of a claim
* @returns {Claim | Error} Parsed claim
*/
function importJsonClaimVersion1 (claimObject) {
if (!('claimVersion' in claimObject && claimObject.claimVersion === 1)) {
return new Error('Invalid claim')
}
const claim = new Claim()
claim._uri = claimObject.uri
claim._fingerprint = claimObject.fingerprint
claim._matches = claimObject.matches.map(x => new ServiceProvider(x))
if (claimObject.status === 'init') {
claim._status = 100
}
if (claimObject.status === 'matched') {
if (claimObject.matches.length === 0) {
claim._status = 301
}
claim._status = 101
}
if (!('result' in claimObject.verification && 'errors' in claimObject.verification)) {
claim._status = 400
}
if (claimObject.verification.errors.length > 0) {
claim._status = 400
}
if (claimObject.verification.result && claimObject.verification.proof.viaProxy) {
claim._status = 201
}
if (claimObject.verification.result && !claimObject.verification.proof.viaProxy) {
claim._status = 200
}
return claim
}
/**
* @ignore
* @param {object} claimObject - JSON representation of a claim
* @returns {Claim | Error} Parsed claim
*/
function importJsonClaimVersion2 (claimObject) {
if (!('claimVersion' in claimObject && claimObject.claimVersion === 2)) {
return new Error('Invalid claim')
}
const claim = new Claim()
claim._uri = claimObject.uri
claim._fingerprint = claimObject.proofs[0]
claim._matches = claimObject.matches.map(x => new ServiceProvider(x))
claim._status = claimObject.status
return claim
}
module.exports = Claim

View file

@ -0,0 +1,114 @@
/*
Copyright 2022 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/?/
const processURI = (uri) => {
return {
serviceprovider: {
type: 'web',
name: 'activitypub'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: uri,
uri,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.ACTIVITYPUB,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: uri
}
}
},
claim: [
{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['summary']
},
{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['attachment', 'value']
},
{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['content']
}
]
}
}
const functions = {
postprocess: (claimData, proofData) => {
claimData.profile.display = `@${proofData.result.preferredUsername}@${new URL(proofData.result.url).hostname}`
return { claimData, proofData }
}
}
const tests = [
{
uri: 'https://domain.org',
shouldMatch: true
},
{
uri: 'https://domain.org/@/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/@alice',
shouldMatch: true
},
{
uri: 'https://domain.org/@alice/123456',
shouldMatch: true
},
{
uri: 'https://domain.org/u/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/users/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/users/alice/123456',
shouldMatch: true
},
{
uri: 'http://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.functions = functions
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/u\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'discourse'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `${match[2]}@${match[1]}`,
uri,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/u/${match[2]}.json`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['user', 'bio_raw']
}]
}
}
const tests = [
{
uri: 'https://domain.org/u/alice',
shouldMatch: true
},
{
uri: 'https://domain.org/u/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,74 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^dns:([a-zA-Z0-9.\-_]*)(?:\?(.*))?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'dns'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri: `https://${match[1]}`,
qr: null
},
proof: {
uri: null,
request: {
fetcher: E.Fetcher.DNS,
access: E.ProofAccess.SERVER,
format: E.ProofFormat.JSON,
data: {
domain: match[1]
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['records', 'txt']
}]
}
}
const tests = [
{
uri: 'dns:domain.org',
shouldMatch: true
},
{
uri: 'dns:domain.org?type=TXT',
shouldMatch: true
},
{
uri: 'https://domain.org',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/(.*)\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'forem'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/articles/${match[2]}/${match[3]}`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['body_markdown']
}]
}
}
const tests = [
{
uri: 'https://domain.org/alice/post',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/post/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,79 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/(.*)\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'forgejo'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/v1/repos/${match[2]}/${match[3]}`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}]
}
}
const tests = [
{
uri: 'https://domain.org/alice/forgejo_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/forgejo_proof/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/other_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -13,66 +13,49 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Gitea service provider ({@link https://docs.keyoxide.org/service-providers/gitea/|Keyoxide docs})
* @module serviceProviders/gitea
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.gitea.processURI('https://domain.example/alice/repo');
*/
const E = require('../enums')
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
const reURI = /^https:\/\/(.*)\/(.*)\/(.*)\/?/
export const reURI = /^https:\/\/(.*)\/(.*)\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const processURI = (uri) => {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'gitea',
name: 'Gitea',
homepage: 'https://about.gitea.com'
return {
serviceprovider: {
type: 'web',
name: 'gitea'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: true
},
proof: {
request: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/v1/repos/${match[2]}/${match[3]}`,
format: E.ProofFormat.JSON
}
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}]
}
})
}
export const tests = [
const tests = [
{
uri: 'https://domain.org/alice/gitea_proof',
shouldMatch: true
@ -90,3 +73,7 @@ export const tests = [
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/gist\.github\.com\/(.*)\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'github'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri: `https://github.com/${match[1]}`,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: `https://api.github.com/gists/${match[2]}`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['files', 'openpgp.md', 'content']
}]
}
}
const tests = [
{
uri: 'https://gist.github.com/Alice/123456789',
shouldMatch: true
},
{
uri: 'https://gist.github.com/Alice/123456789/',
shouldMatch: true
},
{
uri: 'https://domain.org/Alice/123456789',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/(.*)\/gitlab_proof\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'gitlab'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/v4/projects/${match[2]}%2Fgitlab_proof`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}]
}
}
const tests = [
{
uri: 'https://gitlab.domain.org/alice/gitlab_proof',
shouldMatch: true
},
{
uri: 'https://gitlab.domain.org/alice/gitlab_proof/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/other_proof',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/news\.ycombinator\.com\/user\?id=(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'hackernews'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri: `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.HTML,
relation: E.ClaimRelation.CONTAINS,
path: ['about']
}]
}
}
const tests = [
{
uri: 'https://news.ycombinator.com/user?id=Alice',
shouldMatch: true
},
{
uri: 'https://news.ycombinator.com/user?id=Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/user?id=Alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,43 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const data = {
dns: require('./dns'),
irc: require('./irc'),
xmpp: require('./xmpp'),
matrix: require('./matrix'),
telegram: require('./telegram'),
twitter: require('./twitter'),
reddit: require('./reddit'),
liberapay: require('./liberapay'),
lichess: require('./lichess'),
hackernews: require('./hackernews'),
lobsters: require('./lobsters'),
forem: require('./forem'),
// forgejo: require('./forgejo'),
gitea: require('./gitea'),
gitlab: require('./gitlab'),
github: require('./github'),
activitypub: require('./activitypub'),
discourse: require('./discourse'),
owncast: require('./owncast'),
stackexchange: require('./stackexchange'),
keybase: require('./keybase'),
opencollective: require('./opencollective')
}
exports.list = Object.keys(data)
exports.data = data

View file

@ -0,0 +1,79 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^irc:\/\/(.*)\/([a-zA-Z0-9\-[\]\\`_^{|}]*)/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'communication',
name: 'irc'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `irc://${match[1]}/${match[2]}`,
uri,
qr: null
},
proof: {
uri: null,
request: {
fetcher: E.Fetcher.IRC,
access: E.ProofAccess.SERVER,
format: E.ProofFormat.JSON,
data: {
domain: match[1],
nick: match[2]
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: []
}]
}
}
const tests = [
{
uri: 'irc://chat.ircserver.org/Alice1',
shouldMatch: true
},
{
uri: 'irc://chat.ircserver.org/alice?param=123',
shouldMatch: true
},
{
uri: 'irc://chat.ircserver.org/alice_bob',
shouldMatch: true
},
{
uri: 'https://chat.ircserver.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/keybase.io\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'keybase'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri: `https://keybase.io/_/api/1.0/user/lookup.json?username=${match[1]}`,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://keybase.io/_/api/1.0/user/lookup.json?username=${match[1]}`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['them', 'public_keys', 'primary', 'key_fingerprint']
}]
}
}
const tests = [
{
uri: 'https://keybase.io/Alice',
shouldMatch: true
},
{
uri: 'https://keybase.io/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/Alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/liberapay\.com\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'liberapay'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: `https://liberapay.com/${match[1]}/public.json`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['statements', 'content']
}]
}
}
const tests = [
{
uri: 'https://liberapay.com/alice',
shouldMatch: true
},
{
uri: 'https://liberapay.com/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/lichess\.org\/@\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'lichess'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri: `https://lichess.org/api/user/${match[1]}`,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: `https://lichess.org/api/user/${match[1]}`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['profile', 'links']
}]
}
}
const tests = [
{
uri: 'https://lichess.org/@/Alice',
shouldMatch: true
},
{
uri: 'https://lichess.org/@/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/@/Alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/lobste\.rs\/u\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'lobsters'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri: `https://lobste.rs/u/${match[1]}.json`,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://lobste.rs/u/${match[1]}.json`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['about']
}]
}
}
const tests = [
{
uri: 'https://lobste.rs/u/Alice',
shouldMatch: true
},
{
uri: 'https://lobste.rs/u/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/u/Alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -13,25 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Matrix service provider ({@link https://docs.keyoxide.org/service-providers/matrix/|Keyoxide docs})
* @module serviceProviders/matrix
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.matrix.processURI('matrix:u/...');
*/
const E = require('../enums')
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
const reURI = /^matrix:u\/(?:@)?([^@:]*:[^?]*)(\?.*)?/
export const reURI = /^matrix:u\/(?:@)?([^@:]*:[^?]*)(\?.*)?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const processURI = (uri) => {
const match = uri.match(reURI)
if (!match[2]) {
@ -50,45 +36,42 @@ export function processURI (uri) {
const profileUrl = `https://matrix.to/#/@${match[1]}`
const eventUrl = `https://matrix.to/#/${paramRoomId}/${paramEventId}`
return new ServiceProvider({
about: {
id: 'matrix',
name: 'Matrix',
homepage: 'https://matrix.org'
return {
serviceprovider: {
type: 'communication',
name: 'matrix'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `@${match[1]}`,
uri: profileUrl,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: eventUrl,
request: {
fetcher: E.Fetcher.MATRIX,
accessRestriction: E.ProofAccessRestriction.GRANTED,
access: E.ProofAccess.GRANTED,
format: E.ProofFormat.JSON,
data: {
eventId: paramEventId,
roomId: paramRoomId
}
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['content', 'body']
}]
}
})
}
export const tests = [
const tests = [
{
uri:
'matrix:u/alice:matrix.domain.org?org.keyoxide.r=123:domain.org&org.keyoxide.e=123',
@ -112,3 +95,7 @@ export const tests = [
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,75 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/opencollective\.com\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'opencollective'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.GRAPHQL,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: 'https://api.opencollective.com/graphql/v2',
query: `{ "query": "query { collective(slug: \\"${match[1]}\\") { longDescription } }" }`
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['data', 'collective', 'longDescription']
}]
}
}
const tests = [
{
uri: 'https://opencollective.com/Alice',
shouldMatch: true
},
{
uri: 'https://opencollective.com/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/Alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,79 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'owncast'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: match[1],
uri,
qr: null
},
proof: {
uri: `${uri}/api/config`,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: `${uri}/api/config`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['socialHandles', 'url']
}]
}
}
const tests = [
{
uri: 'https://live.domain.org',
shouldMatch: true
},
{
uri: 'https://live.domain.org/',
shouldMatch: true
},
{
uri: 'https://domain.org/live',
shouldMatch: true
},
{
uri: 'https://domain.org/live/',
shouldMatch: true
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -13,66 +13,49 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Reddit service provider ({@link https://docs.keyoxide.org/service-providers/reddit/|Keyoxide docs})
* @module serviceProviders/reddit
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.reddit.processURI('https://reddit.com/...');
*/
const E = require('../enums')
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
const reURI = /^https:\/\/(?:www\.)?reddit\.com\/user\/(.*)\/comments\/(.*)\/(.*)\/?/
export const reURI = /^https:\/\/(?:www\.)?reddit\.com\/user\/(.*)\/comments\/(.*)\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const processURI = (uri) => {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'reddit',
name: 'Reddit',
homepage: 'https://reddit.com'
return {
serviceprovider: {
type: 'web',
name: 'reddit'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri: `https://www.reddit.com/user/${match[1]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://www.reddit.com/user/${match[1]}/comments/${match[2]}.json`,
format: E.ProofFormat.JSON
}
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['data', 'children', 'data', 'selftext']
}]
}
})
}
export const tests = [
const tests = [
{
uri: 'https://www.reddit.com/user/Alice/comments/123456/post',
shouldMatch: true
@ -94,3 +77,7 @@ export const tests = [
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -13,68 +13,51 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* StackExchange service provider ({@link https://docs.keyoxide.org/service-providers/stackexchange/|Keyoxide docs})
* @module serviceProviders/stackexchange
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.stackexchange.processURI('https://stackoverflow.com/users/123/alice');
*/
const E = require('../enums')
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*(?:askubuntu|mathoverflow|serverfault|stackapps|stackoverflow|superuser)|.+\.stackexchange)\.com\/users\/(\d+)/
const reURI = /^https:\/\/(.*(?:askubuntu|mathoverflow|serverfault|stackapps|stackoverflow|superuser)|.+\.stackexchange)\.com\/users\/(\d+)/
const reStackExchange = /\.stackexchange$/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const processURI = (uri) => {
const [, domain, id] = uri.match(reURI)
const site = domain.replace(reStackExchange, '')
return new ServiceProvider({
about: {
id: 'stackexchange',
name: 'Stack Exchange',
homepage: 'https://stackexchange.com'
return {
serviceprovider: {
type: 'web',
name: 'stackexchange'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `${id}@${site}`,
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: `https://${domain}.com/users/${id}?tab=profile`,
request: {
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: `https://api.stackexchange.com/2.3/users/${id}?site=${site}&filter=!AH)b5JqVyImf`,
format: E.ProofFormat.JSON
}
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['items', 'about_me']
}]
}
})
}
export const tests = [
const tests = [
{
uri: 'https://stackoverflow.com/users/1234',
shouldMatch: true
@ -129,3 +112,7 @@ export const tests = [
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,83 @@
/*
Copyright 2022 Maximilian Siling
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /https:\/\/t.me\/([A-Za-z0-9_]{5,32})\?proof=([A-Za-z0-9_]{5,32})/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'communication',
name: 'telegram'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `@${match[1]}`,
uri: `https://t.me/${match[1]}`,
qr: `https://t.me/${match[1]}`
},
proof: {
uri: `https://t.me/${match[2]}`,
request: {
fetcher: E.Fetcher.TELEGRAM,
access: E.ProofAccess.GRANTED,
format: E.ProofFormat.JSON,
data: {
user: match[1],
chat: match[2]
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['text']
}]
}
}
const tests = [
{
uri: 'https://t.me/alice?proof=foobar',
shouldMatch: true
},
{
uri: 'https://t.me/complex_user_1234?proof=complex_chat_1234',
shouldMatch: true
},
{
uri: 'https://t.me/foobar',
shouldMatch: false
},
{
uri: 'https://t.me/foobar?proof=',
shouldMatch: false
},
{
uri: 'https://t.me/?proof=foobar',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,76 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^https:\/\/twitter\.com\/(.*)\/status\/([0-9]*)(?:\?.*)?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'twitter'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `@${match[1]}`,
uri: `https://twitter.com/${match[1]}`,
qr: null
},
proof: {
uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
// Returns an oembed json object with the tweet content in html form
url: `https://publish.twitter.com/oembed?${new URLSearchParams({ url: match[0], omit_script: 1 })}`,
format: E.ProofFormat.JSON
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['html']
}]
}
}
const tests = [
{
uri: 'https://twitter.com/alice/status/1234567890123456789',
shouldMatch: true
},
{
uri: 'https://twitter.com/alice/status/1234567890123456789/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/status/1234567890123456789',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -0,0 +1,74 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const E = require('../enums')
const reURI = /^xmpp:([a-zA-Z0-9.\-_]*)@([a-zA-Z0-9.\-_]*)(?:\?(.*))?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'communication',
name: 'xmpp'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `${match[1]}@${match[2]}`,
uri,
qr: uri
},
proof: {
uri: null,
request: {
fetcher: E.Fetcher.XMPP,
access: E.ProofAccess.SERVER,
format: E.ProofFormat.JSON,
data: {
id: `${match[1]}@${match[2]}`
}
}
},
claim: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: []
}]
}
}
const tests = [
{
uri: 'xmpp:alice@domain.org',
shouldMatch: true
},
{
uri: 'xmpp:alice@domain.org?omemo-sid-123456789=A1B2C3D4E5F6G7H8I9',
shouldMatch: true
},
{
uri: 'https://domain.org',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -1,25 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Contains constant values
* @module constants
*/
/**
* doip.js library version
* @constant {string}
*/
export const version = '1.2.9+myriaiton.1'

View file

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ProxyPolicy } from './enums.js'
const E = require('./enums')
/**
* Contains default values
@ -21,13 +21,31 @@ import { ProxyPolicy } from './enums.js'
*/
/**
* The default claim verification config used throughout the library
* @type {import('./types').VerificationConfig}
* The default options used throughout the library
* @constant {object}
* @property {object} proxy - Options related to the proxy
* @property {string|null} proxy.hostname - The hostname of the proxy
* @property {string} proxy.policy - The policy that defines when to use a proxy ({@link module:enums~ProxyPolicy|here})
* @property {object} claims - Options related to claim verification
* @property {object} claims.activitypub - Options related to the verification of activitypub claims
* @property {string|null} claims.activitypub.url - The URL of the verifier account
* @property {string|null} claims.activitypub.privateKey - The private key to sign the request
* @property {object} claims.irc - Options related to the verification of IRC claims
* @property {string|null} claims.irc.nick - The nick that the library uses to connect to the IRC server
* @property {object} claims.matrix - Options related to the verification of Matrix claims
* @property {string|null} claims.matrix.instance - The server hostname on which the library can log in
* @property {string|null} claims.matrix.accessToken - The access token required to identify the library ({@link https://www.matrix.org/docs/guides/client-server-api|Matrix docs})
* @property {object} claims.telegram - Options related to the verification of Telegram claims
* @property {string|null} claims.telegram.token - The Telegram API's token ({@link https://core.telegram.org/bots/api#authorizing-your-bot|Telegram docs})
* @property {object} claims.xmpp - Options related to the verification of XMPP claims
* @property {string|null} claims.xmpp.service - The server hostname on which the library can log in
* @property {string|null} claims.xmpp.username - The username used to log in
* @property {string|null} claims.xmpp.password - The password used to log in
*/
export const opts = {
const opts = {
proxy: {
hostname: null,
policy: ProxyPolicy.NEVER
policy: E.ProxyPolicy.NEVER
},
claims: {
activitypub: {
@ -35,8 +53,7 @@ export const opts = {
privateKey: null
},
irc: {
nick: null,
sasl: []
nick: null
},
matrix: {
instance: null,
@ -52,3 +69,5 @@ export const opts = {
}
}
}
exports.opts = opts

View file

@ -23,7 +23,7 @@ limitations under the License.
* @readonly
* @enum {string}
*/
export const ProxyPolicy = {
const ProxyPolicy = {
/** Proxy usage decision depends on environment and service provider */
ADAPTIVE: 'adaptive',
/** Always use a proxy */
@ -31,17 +31,16 @@ export const ProxyPolicy = {
/** Never use a proxy, skip a verification if a proxy is inevitable */
NEVER: 'never'
}
Object.freeze(ProxyPolicy)
/**
* Methods for fetching proofs
* @readonly
* @enum {string}
*/
export const Fetcher = {
const Fetcher = {
/** HTTP requests to ActivityPub */
ACTIVITYPUB: 'activitypub',
/** ASPE HTTP requests */
ASPE: 'aspe',
/** DNS module from Node.js */
DNS: 'dns',
/** GraphQL over HTTP requests */
@ -52,20 +51,19 @@ export const Fetcher = {
IRC: 'irc',
/** HTTP request to Matrix API */
MATRIX: 'matrix',
/** HKP and WKS request for OpenPGP */
OPENPGP: 'openpgp',
/** HTTP request to Telegram API */
TELEGRAM: 'telegram',
/** XMPP module from Node.js */
XMPP: 'xmpp'
}
Object.freeze(Fetcher)
/**
* Entity encoding format
* @readonly
* @enum {string}
*/
export const EntityEncodingFormat = {
const EntityEncodingFormat = {
/** No special formatting */
PLAIN: 'plain',
/** HTML encoded entities */
@ -73,15 +71,16 @@ export const EntityEncodingFormat = {
/** XML encoded entities */
XML: 'xml'
}
Object.freeze(EntityEncodingFormat)
/**
* Levels of access restriction for proof fetching
* @readonly
* @enum {string}
*/
export const ProofAccessRestriction = {
const ProofAccess = {
/** Any HTTP request will work */
NONE: 'none',
GENERIC: 'generic',
/** CORS requests are denied */
NOCORS: 'nocors',
/** HTTP requests must contain API or access tokens */
@ -89,37 +88,40 @@ export const ProofAccessRestriction = {
/** Not accessible by HTTP request, needs server software */
SERVER: 'server'
}
Object.freeze(ProofAccess)
/**
* Format of proof
* @readonly
* @enum {string}
*/
export const ProofFormat = {
const ProofFormat = {
/** JSON format */
JSON: 'json',
/** Plaintext format */
TEXT: 'text'
}
Object.freeze(ProofFormat)
/**
* Format of claim
* @readonly
* @enum {string}
*/
export const ClaimFormat = {
const ClaimFormat = {
/** `openpgp4fpr:123123123` */
URI: 'uri',
/** `123123123` */
FINGERPRINT: 'fingerprint'
}
Object.freeze(ClaimFormat)
/**
* How to find the proof inside the fetched data
* How to find the claim inside the proof's JSON data
* @readonly
* @enum {string}
*/
export const ClaimRelation = {
const ClaimRelation = {
/** Claim is somewhere in the JSON field's textual content */
CONTAINS: 'contains',
/** Claim is equal to the JSON field's textual content */
@ -127,87 +129,28 @@ export const ClaimRelation = {
/** Claim is equal to an element of the JSON field's array of strings */
ONEOF: 'oneof'
}
Object.freeze(ClaimRelation)
/**
* Status of the Claim instance
* @readonly
* @enum {number}
* @enum {string}
*/
export const ClaimStatus = {
const ClaimStatus = {
/** Claim has been initialized */
INIT: 100,
INIT: 'init',
/** Claim has matched its URI to candidate claim definitions */
MATCHED: 101,
/** Claim was successfully verified */
VERIFIED: 200,
/** Claim was successfully verified using proxied data */
VERIFIED_VIA_PROXY: 201,
/** Unknown matching error */
MATCHING_ERROR: 300,
/** No matched service providers */
NO_MATCHES: 301,
/** Unknown matching error */
VERIFICATION_ERROR: 400,
/** No proof found in data returned by service providers */
NO_PROOF_FOUND: 401
MATCHED: 'matched',
/** Claim has verified one or multiple candidate claim definitions */
VERIFIED: 'verified'
}
Object.freeze(ClaimStatus)
/**
* Profile type
* @readonly
* @enum {string}
*/
export const ProfileType = {
/** ASP profile */
ASP: 'asp',
/** OpenPGP profile */
OPENPGP: 'openpgp'
}
/**
* Public key type
* @readonly
* @enum {string}
*/
export const PublicKeyType = {
EDDSA: 'eddsa',
ES256: 'es256',
OPENPGP: 'openpgp',
UNKNOWN: 'unknown',
NONE: 'none'
}
/**
* Public key format
* @readonly
* @enum {string}
*/
export const PublicKeyEncoding = {
PEM: 'pem',
JWK: 'jwk',
ARMORED_PGP: 'armored_pgp',
NONE: 'none'
}
/**
* Method to fetch the public key
* @readonly
* @enum {string}
*/
export const PublicKeyFetchMethod = {
ASPE: 'aspe',
HKP: 'hkp',
WKD: 'wkd',
HTTP: 'http',
NONE: 'none'
}
/**
* Protocol to query OpenPGP public keys
* @readonly
* @enum {string}
*/
export const OpenPgpQueryProtocol = {
HKP: 'hkp',
WKD: 'wkd'
}
exports.ProxyPolicy = ProxyPolicy
exports.Fetcher = Fetcher
exports.EntityEncodingFormat = EntityEncodingFormat
exports.ProofAccess = ProofAccess
exports.ProofFormat = ProofFormat
exports.ClaimFormat = ClaimFormat
exports.ClaimRelation = ClaimRelation
exports.ClaimStatus = ClaimStatus

View file

@ -13,43 +13,45 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const axios = require('axios').default
const validator = require('validator').default
const jsEnv = require('browser-or-node')
/**
* Fetch proofs using ActivityPub HTTP requests
* @module fetcher/activitypub
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.activitypub.fn({ url: 'https://domain.example/@alice' });
*/
import axios from 'axios'
import isURL from 'validator/lib/isURL.js'
import { isNode } from 'browser-or-node'
import crypto from 'crypto'
import { version } from '../constants.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.url - The URL of the account to verify
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object>} The fetched ActivityPub object
* @param {object} opts - Options used to enable the request
* @param {object} opts.claims
* @param {object} opts.claims.activitypub
* @param {string} opts.claims.activitypub.url - The URL of the verifier account
* @param {string} opts.claims.activitypub.privateKey - The private key to sign the request
* @returns {Promise<object>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
let crypto
if (jsEnv.isNode) {
crypto = require('crypto')
}
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
@ -57,7 +59,7 @@ export async function fn (data, opts) {
(async () => {
let isConfigured = false
try {
isURL(opts.claims.activitypub.url)
validator.isURL(opts.claims.activitypub.url)
isConfigured = true
} catch (_) {}
@ -69,10 +71,10 @@ export async function fn (data, opts) {
date: now.toUTCString(),
accept: 'application/activity+json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
'User-Agent': `doipjs/${require('../../package.json').version}`
}
if (isConfigured && isNode) {
if (isConfigured && jsEnv.isNode) {
// Generate the signature
const signedString = `(request-target): get ${pathname}${search}\nhost: ${host}\ndate: ${now.toUTCString()}`
const sign = crypto.createSign('SHA256')
@ -98,7 +100,8 @@ export async function fn (data, opts) {
})()
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -1,90 +0,0 @@
/*
Copyright 2024 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Fetch proofs from Profile obtained through ASPE
* @module fetcher/aspe
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.aspe.fn({ aspeUri: 'aspe:domain.example:abc123def456' });
*/
import axios from 'axios'
import isFQDN from 'validator/lib/isFQDN.js'
import { version } from '../constants.js'
import { parseProfileJws } from '../asp.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
export const timeout = 5000
const reURI = /^aspe:([a-zA-Z0-9.\-_]*):([a-zA-Z0-9]*)/
/**
* Execute a fetch request
* @function
* @param {object} data - Data used in the request
* @param {string} data.aspeUri - ASPE URI of the targeted profile
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object>} The fetched claims from an ASP profile
*/
export async function fn (data, opts) {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
const match = data.aspeUri.match(reURI)
if (!data.aspeUri || !reURI.test(data.aspeUri) || !isFQDN(match[1])) {
reject(new Error('No valid ASPE URI provided'))
return
}
const url = `https://${match[1]}/.well-known/aspe/id/${match[2].toUpperCase()}`
axios.get(url, {
headers: {
Accept: 'application/asp+jwt',
'User-Agent': `doipjs/${version}`
},
validateStatus: (status) => status >= 200 && status < 400
})
.then(async res => await parseProfileJws(res.data, data.aspeUri))
.then(profile =>
profile.personas.flatMap(p => { return p.claims.map(c => c._uri) })
)
.then(res => {
resolve({
claims: res
})
})
.catch(e => {
reject(e)
})
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

View file

@ -13,44 +13,36 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const jsEnv = require('browser-or-node')
/**
* Fetch proofs using DNS TXT records
* @module fetcher/dns
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.dns.fn({ domain: 'domain.example' });
*/
import { isBrowser } from 'browser-or-node'
import dns from 'dns'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
/**
if (jsEnv.isNode) {
const dns = require('dns')
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.domain - The targeted domain
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object>} The fetched DNS records
* @returns {Promise<object>}
*/
export async function fn (data, opts) {
if (isBrowser) {
return null
}
module.exports.fn = async (data, opts) => {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
@ -70,7 +62,11 @@ export async function fn (data, opts) {
})
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}
} else {
module.exports.fn = null
}

View file

@ -13,41 +13,34 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const axios = require('axios').default
/**
* Fetch proofs using GraphQL queries
* @module fetcher/graphql
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.graphql.fn({ url: 'https://domain.example/graphql/v2', query: '{ "query": "..." }' });
*/
import axios from 'axios'
import { version } from '../constants.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
/**
* Execute a GraphQL query via HTTP request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.url - The URL pointing at the GraphQL HTTP endpoint
* @param {string} data.query - The GraphQL query to fetch the data containing the proof
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object>} The fetched GraphQL object
* @returns {Promise<object|string>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
@ -68,7 +61,7 @@ export async function fn (data, opts) {
headers: {
'Content-Type': 'application/json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
'User-Agent': `doipjs/${require('../../package.json').version}`
},
validateStatus: function (status) {
return status >= 200 && status < 400
@ -82,7 +75,8 @@ export async function fn (data, opts) {
})
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -13,42 +13,35 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const axios = require('axios').default
const E = require('../enums')
/**
* Fetch proofs using HTTP requests
* @module fetcher/http
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.http.fn({ url: 'https://domain.example/data.json', format: 'json' });
*/
import axios from 'axios'
import { ProofFormat } from '../enums.js'
import { version } from '../constants.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.url - The URL pointing at targeted content
* @param {string} data.format - The format of the targeted content
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object|string>} The fetched JSON object or text
* @returns {Promise<object|string>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
@ -59,12 +52,12 @@ export async function fn (data, opts) {
}
switch (data.format) {
case ProofFormat.JSON:
case E.ProofFormat.JSON:
axios.get(data.url, {
headers: {
Accept: 'application/json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
'User-Agent': `doipjs/${require('../../package.json').version}`
},
validateStatus: function (status) {
return status >= 200 && status < 400
@ -77,7 +70,7 @@ export async function fn (data, opts) {
reject(e)
})
break
case ProofFormat.TEXT:
case E.ProofFormat.TEXT:
axios.get(data.url, {
validateStatus: function (status) {
return status >= 200 && status < 400
@ -97,7 +90,8 @@ export async function fn (data, opts) {
}
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -13,13 +13,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * as activitypub from './activitypub.js'
export * as aspe from './aspe.js'
export * as dns from './dns.js'
export * as graphql from './graphql.js'
export * as http from './http.js'
export * as irc from './irc.js'
export * as matrix from './matrix.js'
export * as openpgp from './openpgp.js'
export * as telegram from './telegram.js'
export * as xmpp from './xmpp.js'
exports.activitypub = require('./activitypub')
exports.dns = require('./dns')
exports.graphql = require('./graphql')
exports.http = require('./http')
exports.irc = require('./irc')
exports.matrix = require('./matrix')
exports.telegram = require('./telegram')
exports.xmpp = require('./xmpp')

View file

@ -1,22 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export * as activitypub from './activitypub.js'
export * as aspe from './aspe.js'
export * as graphql from './graphql.js'
export * as http from './http.js'
export * as matrix from './matrix.js'
export * as openpgp from './openpgp.js'
export * as telegram from './telegram.js'

View file

@ -13,73 +13,61 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const jsEnv = require('browser-or-node')
/**
* Fetch proofs using IRC
* @module fetcher/irc
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.irc.fn({ nick: 'alice', domain: 'domain.example' });
*/
import irc from 'irc-upd'
import isAscii from 'validator/lib/isAscii.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 20000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 20000
module.exports.timeout = 20000
/**
if (jsEnv.isNode) {
const irc = require('irc-upd')
const validator = require('validator').default
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.nick - The nick of the targeted account
* @param {string} data.domain - The domain on which the targeted account is registered
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<Array<string>>} The fetched proofs from an IRC account
* @param {object} opts - Options used to enable the request
* @param {object} opts.claims
* @param {object} opts.claims.irc
* @param {string} opts.claims.irc.nick - The nick to be used by the library to log in
* @returns {Promise<object>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
try {
isAscii(opts.claims.irc.nick)
validator.isAscii(opts.claims.irc.nick)
} catch (err) {
throw new Error(`IRC fetcher was not set up properly (${err.message})`)
}
try {
// Add sasl-related config if the server matches
const matchedSaslConfig = opts.claims.irc.sasl.find(saslConfig => data.domain.match(new RegExp(saslConfig.domainRegex)) !== null)
const saslOptions = matchedSaslConfig
? {
sasl: true,
userName: matchedSaslConfig.username,
password: matchedSaslConfig.password
}
: {
sasl: false
}
const client = new irc.Client(data.domain, opts.claims.irc.nick, {
port: 6697,
secure: true,
channels: [],
showErrors: false,
debug: false,
...saslOptions
debug: false
})
const reKey = /[a-zA-Z0-9\-_]+\s+:\s((?:openpgp4fpr|aspe):.*)/
const reKey = /[a-zA-Z0-9\-_]+\s+:\s(openpgp4fpr:.*)/
const reEnd = /End\sof\s.*\staxonomy./
const keys = []
@ -103,7 +91,11 @@ export async function fn (data, opts) {
}
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}
} else {
module.exports.fn = null
}

View file

@ -13,50 +13,47 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const axios = require('axios').default
const validator = require('validator').default
/**
* Fetch proofs using Matrix messages
* @module fetcher/matrix
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.matrix.fn({ eventId: '$abc123def456', roomId: '!dBfQZxCoGVmSTujfiv:matrix.org' });
*/
import axios from 'axios'
import isFQDN from 'validator/lib/isFQDN.js'
import isAscii from 'validator/lib/isAscii.js'
import { version } from '../constants.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.eventId - The identifier of the targeted post
* @param {string} data.roomId - The identifier of the room containing the targeted post
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object>} The fetched Matrix object
* @param {object} opts - Options used to enable the request
* @param {object} opts.claims
* @param {object} opts.claims.matrix
* @param {string} opts.claims.matrix.instance - The server hostname on which the library can log in
* @param {string} opts.claims.matrix.accessToken - The access token required to identify the library ({@link https://www.matrix.org/docs/guides/client-server-api|Matrix docs})
* @returns {Promise<object>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
try {
isFQDN(opts.claims.matrix.instance)
isAscii(opts.claims.matrix.accessToken)
validator.isFQDN(opts.claims.matrix.instance)
validator.isAscii(opts.claims.matrix.accessToken)
} catch (err) {
throw new Error(`Matrix fetcher was not set up properly (${err.message})`)
}
@ -67,7 +64,7 @@ export async function fn (data, opts) {
headers: {
Accept: 'application/json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
'User-Agent': `doipjs/${require('../../package.json').version}`
}
})
.then(res => {
@ -81,7 +78,8 @@ export async function fn (data, opts) {
})
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -1,131 +0,0 @@
/*
Copyright 2024 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Fetch proofs from OpenPGP notations
* @module fetcher/openpgp
* @example
* import { fetcher, enums as E } from 'doipjs';
*
* const hkpProtocol = E.OpenPgpQueryProtocol.HKP;
* const hkpUrl = 'https://keys.openpgp.org/vks/v1/by-fingerprint/ABC123DEF456';
* const hkpData = await fetcher.openpgp.fn({ url: hkpUrl, protocol: hkpProtocol });
*
* const wkdProtocol = E.OpenPgpQueryProtocol.WKD;
* const wkdUrl = 'https://domain.example/.well-known/openpgpkey/hu/kei1q4tipxxu1yj79k9kfukdhfy631xe?l=alice';
* const wkdData = await fetcher.openpgp.fn({ url: wkdUrl, protocol: wkdProtocol });
*/
import axios from 'axios'
import { readKey } from 'openpgp'
import { OpenPgpQueryProtocol } from '../enums.js'
import { version } from '../constants.js'
import { parsePublicKey } from '../openpgp.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
export const timeout = 5000
/**
* Execute a fetch request
* @function
* @param {object} data - Data used in the request
* @param {string} data.url - The URL pointing at targeted content
* @param {OpenPgpQueryProtocol} data.protocol - The protocol used to access the targeted content
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object>} The fetched notations from an OpenPGP key
*/
export async function fn (data, opts) {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
if (!data.url) {
reject(new Error('No valid URI provided'))
return
}
switch (data.protocol) {
case OpenPgpQueryProtocol.HKP:
axios.get(data.url, {
headers: {
Accept: 'application/pgp-keys',
'User-Agent': `doipjs/${version}`
},
validateStatus: (status) => status >= 200 && status < 400
})
.then(res => res.data)
.then(async data => await readKey({ armoredKey: data }))
.then(async publicKey => await parsePublicKey(publicKey))
.then(profile =>
profile.personas.flatMap(p => { return p.claims.map(c => c._uri) })
)
.then(res => {
resolve({
notations: {
'proof@ariadne.id': res
}
})
})
.catch(e => {
reject(e)
})
break
case OpenPgpQueryProtocol.WKD:
axios.get(data.url, {
headers: {
Accept: 'application/octet-stream',
'User-Agent': `doipjs/${version}`
},
responseType: 'arraybuffer',
validateStatus: (status) => status >= 200 && status < 400
})
.then(res => res.data)
.then(async data => await readKey({ binaryKey: data }))
.then(async publicKey => await parsePublicKey(publicKey))
.then(profile =>
profile.personas.flatMap(p => { return p.claims.map(c => c._uri) })
)
.then(res => {
resolve({
notations: {
'proof@ariadne.id': res
}
})
})
.catch(e => {
reject(e)
})
break
default:
reject(new Error('Unsupported OpenPGP query protocol'))
break
}
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

View file

@ -13,48 +13,46 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const axios = require('axios').default
const validator = require('validator').default
/**
* Fetch proofs using Telegram groups
* @module fetcher/telegram
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.telegram.fn({ user: 'alice', chat: 'alice_identity_proof' });
*/
import axios from 'axios'
import isAscii from 'validator/lib/isAscii.js'
import { version } from '../constants.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The single request's timeout value in milliseconds
* This fetcher makes two requests in total
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.chat - Telegram public group name (slug)
* @param {string} data.user - Telegram username
* @param {string} data.chat - Telegram public chat username
* @param {string} data.user - Telegram user username
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<object|string>} The fetched Telegram object
* @param {object} opts - Options used to enable the request
* @param {object} opts.claims
* @param {object} opts.claims.telegram
* @param {string} opts.claims.telegram.token - The Telegram Bot API token
* @returns {Promise<object|string>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
const apiPromise = (/** @type {string} */ method) => new Promise((resolve, reject) => {
try {
isAscii(opts.claims.telegram.token)
validator.isAscii(opts.claims.telegram.token)
} catch (err) {
throw new Error(`Telegram fetcher was not set up properly (${err.message})`)
}
@ -69,7 +67,7 @@ export async function fn (data, opts) {
headers: {
Accept: 'application/json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
'User-Agent': `doipjs/${require('../../package.json').version}`
},
validateStatus: (status) => status === 200
})
@ -109,7 +107,8 @@ export async function fn (data, opts) {
})
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -13,40 +13,33 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const jsEnv = require('browser-or-node')
/**
* Fetch proofs from XMPP accounts
* @module fetcher/xmpp
* @example
* import { fetcher } from 'doipjs';
* const data = await fetcher.xmpp.fn({ id: 'alice@domain.example' });
*/
import { client, xml } from '@xmpp/client'
import debug from '@xmpp/debug'
import isFQDN from 'validator/lib/isFQDN.js'
import isAscii from 'validator/lib/isAscii.js'
/**
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
export const timeout = 5000
module.exports.timeout = 5000
let xmpp = null
let iqCaller = null
if (jsEnv.isNode) {
const { client, xml } = require('@xmpp/client')
const debug = require('@xmpp/debug')
const validator = require('validator').default
/**
* Start the XMPP client
* @ignore
* @function
* @param {import('../types').XmppClaimVerificationConfig} params - XMPP claim verification config
* @returns {Promise<object>} The fetched proofs from an XMPP account
*/
const xmppStart = async (params) => {
let xmpp = null
let iqCaller = null
const xmppStart = async (service, username, password) => {
return new Promise((resolve, reject) => {
const xmpp = client({ ...params })
const xmpp = client({
service,
username,
password
})
if (process.env.NODE_ENV !== 'production') {
debug(xmpp, true)
}
@ -59,28 +52,38 @@ const xmppStart = async (params) => {
reject(error)
})
})
}
}
/**
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.id - The identifier of the targeted account
* @param {number} [data.fetcherTimeout] - Optional timeout for the fetcher
* @param {import('../types').VerificationConfig} [opts] - Options used to enable the request
* @returns {Promise<Array<string>>} The fetched proofs from an XMPP account
* @param {object} opts - Options used to enable the request
* @param {object} opts.claims
* @param {object} opts.claims.xmpp
* @param {string} opts.claims.xmpp.service - The server hostname on which the library can log in
* @param {string} opts.claims.xmpp.username - The username used to log in
* @param {string} opts.claims.xmpp.password - The password used to log in
* @returns {Promise<object>}
*/
export async function fn (data, opts) {
module.exports.fn = async (data, opts) => {
try {
isFQDN(opts.claims.xmpp.service)
isAscii(opts.claims.xmpp.username)
isAscii(opts.claims.xmpp.password)
validator.isFQDN(opts.claims.xmpp.service)
validator.isAscii(opts.claims.xmpp.username)
validator.isAscii(opts.claims.xmpp.password)
} catch (err) {
throw new Error(`XMPP fetcher was not set up properly (${err.message})`)
}
if (!xmpp || xmpp.status !== 'online') {
const xmppStartRes = await xmppStart(opts.claims.xmpp)
const xmppStartRes = await xmppStart(
opts.claims.xmpp.service,
opts.claims.xmpp.username,
opts.claims.xmpp.password
)
xmpp = xmppStartRes.xmpp
iqCaller = xmppStartRes.iqCaller
}
@ -89,7 +92,7 @@ export async function fn (data, opts) {
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : timeout
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
})
@ -183,7 +186,11 @@ export async function fn (data, opts) {
})()
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}
} else {
module.exports.fn = null
}

View file

@ -13,23 +13,30 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const Profile = require('./profile')
const Persona = require('./persona')
const Claim = require('./claim')
const claimDefinitions = require('./claimDefinitions')
const proofs = require('./proofs')
const keys = require('./keys')
const asp = require('./asp')
const signatures = require('./signatures')
const enums = require('./enums')
const defaults = require('./defaults')
const utils = require('./utils')
const verifications = require('./verifications')
const fetcher = require('./fetcher')
/**
* @module doipjs
* @license Apache-2.0
*/
export { Profile } from './profile.js'
export { Persona } from './persona.js'
export { Claim } from './claim.js'
export { ServiceProvider } from './serviceProvider.js'
export * as ServiceProviderDefinitions from './serviceProviders/index.js'
export * as proofs from './proofs.js'
export * as openpgp from './openpgp.js'
export * as asp from './asp.js'
export * as signatures from './signatures.js'
export * as enums from './enums.js'
export * as defaults from './defaults.js'
export * as utils from './utils.js'
export * as verifications from './verifications.js'
export * as schemas from './schemas.js'
export * as fetcher from './fetcher/index.js'
exports.Profile = Profile
exports.Persona = Persona
exports.Claim = Claim
exports.claimDefinitions = claimDefinitions
exports.proofs = proofs
exports.keys = keys
exports.asp = asp
exports.signatures = signatures
exports.enums = enums
exports.defaults = defaults
exports.utils = utils
exports.verifications = verifications
exports.fetcher = fetcher

View file

@ -13,100 +13,88 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import axios from 'axios'
import { isUri } from 'valid-url'
import { readKey, PublicKey } from 'openpgp'
import HKP from '@openpgp/hkp-client'
import WKD from '@openpgp/wkd-client'
import { Claim } from './claim.js'
import { ProfileType, PublicKeyEncoding, PublicKeyFetchMethod, PublicKeyType } from './enums.js'
import { Profile } from './profile.js'
import { Persona } from './persona.js'
const axios = require('axios').default
const validUrl = require('valid-url')
const openpgp = require('openpgp')
const HKP = require('@openpgp/hkp-client')
const WKD = require('@openpgp/wkd-client')
const Claim = require('./claim')
/**
* Functions related to OpenPGP Profiles
* @module openpgp
* Functions related to the fetching and handling of keys
* @module keys
*/
/**
* Fetch a public key using keyservers
* @function
* @param {string} identifier - Fingerprint or email address
* @param {string} [keyserverDomain] - Domain of the keyserver
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver
* @returns {Promise<openpgp.PublicKey>}
* @example
* const key1 = doip.keys.fetchHKP('alice@domain.tld');
* const key2 = doip.keys.fetchHKP('123abc123abc');
* const key3 = doip.keys.fetchHKP('123abc123abc', 'pgpkeys.eu');
*/
export async function fetchHKP (identifier, keyserverDomain = 'keys.openpgp.org') {
const keyserverBaseUrl = `https://${keyserverDomain ?? 'keys.openpgp.org'}`
const fetchHKP = async (identifier, keyserverDomain) => {
const keyserverBaseUrl = keyserverDomain
? `https://${keyserverDomain}`
: 'https://keys.openpgp.org'
// @ts-ignore
const hkp = new HKP(keyserverBaseUrl)
const lookupOpts = {
query: identifier
}
const publicKeyArmored = await hkp
const publicKey = await hkp
.lookup(lookupOpts)
.catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
if (!publicKeyArmored) {
if (!publicKey) {
throw new Error('Key does not exist or could not be fetched')
}
const publicKey = await readKey({
armoredKey: publicKeyArmored
return await openpgp.readKey({
armoredKey: publicKey
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.fetch.method = PublicKeyFetchMethod.HKP
profile.publicKey.fetch.query = identifier
return profile
}
/**
* Fetch a public key using Web Key Directory
* @function
* @param {string} identifier - Identifier of format 'username@domain.tld`
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @returns {Promise<openpgp.PublicKey>}
* @example
* const key = doip.keys.fetchWKD('alice@domain.tld');
*/
export async function fetchWKD (identifier) {
const fetchWKD = async (identifier) => {
// @ts-ignore
const wkd = new WKD()
const lookupOpts = {
email: identifier
}
const publicKeyBinary = await wkd
const publicKey = await wkd
.lookup(lookupOpts)
.catch((/** @type {Error} */ error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
if (!publicKeyBinary) {
if (!publicKey) {
throw new Error('Key does not exist or could not be fetched')
}
const publicKey = await readKey({
binaryKey: publicKeyBinary
return await openpgp.readKey({
binaryKey: publicKey
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.fetch.method = PublicKeyFetchMethod.WKD
profile.publicKey.fetch.query = identifier
return profile
}
/**
@ -114,11 +102,11 @@ export async function fetchWKD (identifier) {
* @function
* @param {string} username - Keybase username
* @param {string} fingerprint - Fingerprint of key
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @returns {Promise<openpgp.PublicKey>}
* @example
* const key = doip.keys.fetchKeybase('alice', '123abc123abc');
*/
export async function fetchKeybase (username, fingerprint) {
const fetchKeybase = async (username, fingerprint) => {
const keyLink = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
let rawKeyContent
try {
@ -138,26 +126,19 @@ export async function fetchKeybase (username, fingerprint) {
throw new Error(`Error fetching Keybase key: ${e.message}`)
}
const publicKey = await readKey({
return await openpgp.readKey({
armoredKey: rawKeyContent
})
.catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.fetch.method = PublicKeyFetchMethod.HTTP
profile.publicKey.fetch.query = null
profile.publicKey.fetch.resolvedUrl = keyLink
return profile
}
/**
* Get a public key from armored public key text data
* Get a public key from plaintext data
* @function
* @param {string} rawKeyContent - Plaintext ASCII-formatted public key data
* @returns {Promise<Profile>} The profile from the armored public key
* @returns {Promise<openpgp.PublicKey>}
* @example
* const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
*
@ -167,31 +148,29 @@ export async function fetchKeybase (username, fingerprint) {
* -----END PGP PUBLIC KEY BLOCK-----`
* const key = doip.keys.fetchPlaintext(plainkey);
*/
export async function fetchPlaintext (rawKeyContent) {
const publicKey = await readKey({
const fetchPlaintext = async (rawKeyContent) => {
const publicKey = await openpgp.readKey({
armoredKey: rawKeyContent
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
return profile
return publicKey
}
/**
* Fetch a public key using an URI
* @function
* @param {string} uri - URI that defines the location of the key
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @returns {Promise<openpgp.PublicKey>}
* @example
* const key1 = doip.keys.fetchURI('hkp:alice@domain.tld');
* const key2 = doip.keys.fetchURI('hkp:123abc123abc');
* const key3 = doip.keys.fetchURI('wkd:alice@domain.tld');
*/
export async function fetchURI (uri) {
if (!isUri(uri)) {
const fetchURI = async (uri) => {
if (!validUrl.isUri(uri)) {
throw new Error('Invalid URI')
}
@ -230,103 +209,115 @@ export async function fetchURI (uri) {
* This function will also try and parse the input as a plaintext key
* @function
* @param {string} identifier - URI that defines the location of the key
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @returns {Promise<openpgp.PublicKey>}
* @example
* const key1 = doip.keys.fetch('alice@domain.tld');
* const key2 = doip.keys.fetch('123abc123abc');
*/
export async function fetch (identifier) {
const fetch = async (identifier) => {
const re = /([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/
const match = identifier.match(re)
let profile = null
let pubKey = null
// Attempt plaintext
if (!pubKey) {
try {
profile = await fetchPlaintext(identifier)
pubKey = await fetchPlaintext(identifier)
} catch (e) {}
}
// Attempt WKD
if (!profile && identifier.includes('@')) {
if (!pubKey && identifier.includes('@')) {
try {
profile = await fetchWKD(match[1])
pubKey = await fetchWKD(match[1])
} catch (e) {}
}
// Attempt HKP
if (!profile) {
profile = await fetchHKP(
if (!pubKey) {
pubKey = await fetchHKP(
match[2] ? match[2] : match[1],
match[2] ? match[1] : null
)
}
if (!profile) {
if (!pubKey) {
throw new Error('Key does not exist or could not be fetched')
}
return profile
return pubKey
}
/**
* Process a public key to get a profile
* Process a public key to get user data and claims
* @function
* @param {PublicKey} publicKey - The public key to parse
* @returns {Promise<Profile>} The profile from the processed OpenPGP key
* @param {openpgp.PublicKey} publicKey - The public key to process
* @returns {Promise<object>}
* @example
* const key = doip.keys.fetchURI('hkp:alice@domain.tld');
* const profile = doip.keys.parsePublicKey(key);
* profile.personas[0].claims.forEach(claim => {
* const data = doip.keys.process(key);
* data.users[0].claims.forEach(claim => {
* console.log(claim.uri);
* });
*/
export async function parsePublicKey (publicKey) {
if (!(publicKey && (publicKey instanceof PublicKey))) {
const process = async (publicKey) => {
if (!(publicKey && (publicKey instanceof openpgp.PublicKey))) {
throw new Error('Invalid public key')
}
const fingerprint = publicKey.getFingerprint()
const primaryUser = await publicKey.getPrimaryUser()
const users = publicKey.users
const personas = []
const usersOutput = []
users.forEach((user, i) => {
if (!user.userID) return
const pe = new Persona(user.userID.name, [])
pe.setIdentifier(user.userID.userID)
pe.setDescription(user.userID.comment)
pe.setEmailAddress(user.userID.email)
usersOutput[i] = {
userData: {
id: user.userID ? user.userID.userID : null,
name: user.userID ? user.userID.name : null,
email: user.userID ? user.userID.email : null,
comment: user.userID ? user.userID.comment : null,
isPrimary: primaryUser.index === i,
isRevoked: false
},
claims: []
}
if ('selfCertifications' in user && user.selfCertifications.length > 0) {
const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0]
if (selfCertification.revoked) {
pe.revoke()
}
const notations = selfCertification.rawNotations
pe.claims = notations
usersOutput[i].claims = notations
.filter(
({ name, humanReadable }) =>
humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
)
.map(
({ value }) =>
new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
new Claim(new TextDecoder().decode(value), fingerprint)
)
}
personas.push(pe)
usersOutput[i].userData.isRevoked = selfCertification.revoked
}
})
const profile = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, personas)
profile.primaryPersonaIndex = primaryUser.index
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.fingerprint = fingerprint
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
return profile
return {
fingerprint,
users: usersOutput,
primaryUserIndex: primaryUser.index,
key: {
data: publicKey,
fetchMethod: null,
uri: null
}
}
}
exports.fetchHKP = fetchHKP
exports.fetchWKD = fetchWKD
exports.fetchKeybase = fetchKeybase
exports.fetchPlaintext = fetchPlaintext
exports.fetchURI = fetchURI
exports.fetch = fetch
exports.process = process

View file

@ -13,191 +13,44 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Claim } from './claim.js'
// eslint-disable-next-line
const Claim = require('./claim')
/**
* A persona with identity claims
* @class
* @classdesc A persona with identity claims
* @constructor
* @public
* @example
* const claim = Claim('https://alice.tld', '123');
* const pers = Persona('Alice', 'About Alice', [claim]);
*/
export class Persona {
class Persona {
/**
* @param {string} name - Name of the persona
* @param {Array<Claim>} claims - Claims of the persona
* @param {string} name
* @param {string} [description]
* @param {Claim[]} [claims]
*/
constructor (name, claims) {
/**
* Identifier of the persona
* @type {string | null}
* @public
*/
this.identifier = null
constructor (name, description, claims) {
/**
* Name to be displayed on the profile page
* @type {string}
* @public
*/
this.name = name
/**
* Email address of the persona
* @type {string | null}
* @public
*/
this.email = null
/**
* Description to be displayed on the profile page
* @type {string | null}
* @type {string}
* @public
*/
this.description = null
/**
* URL to an avatar image
* @type {string | null}
* @public
*/
this.avatarUrl = null
/**
* Theme color
* @type {string | null}
* @public
*/
this.themeColor = null
this.description = description
/**
* List of identity claims
* @type {Array<Claim>}
* @public
*/
this.claims = claims
/**
* Has the persona been revoked
* @type {boolean}
* @public
*/
this.isRevoked = false
}
/**
* Parse a JSON object and convert it into a persona
* @function
* @param {object} personaObject - JSON representation of a persona
* @param {number} profileVersion - Version of the Profile containing the persona
* @returns {Persona | Error} Parsed persona
* @example
* doip.Persona.fromJSON(JSON.stringify(persona), 2);
*/
static fromJSON (personaObject, profileVersion) {
/** @type {Persona} */
let persona
let result
if (typeof personaObject === 'object' && profileVersion) {
switch (profileVersion) {
case 2:
result = importJsonPersonaVersion2(personaObject)
if (result instanceof Error) {
throw result
}
persona = result
break
default:
throw new Error('Invalid persona version')
}
}
return persona
}
/**
* Set the persona's identifier
* @function
* @param {string} identifier - Identifier of the persona
*/
setIdentifier (identifier) {
this.identifier = identifier
}
/**
* Set the persona's description
* @function
* @param {string} description - Description of the persona
*/
setDescription (description) {
this.description = description
}
/**
* Set the persona's email address
* @function
* @param {string} email - Email address of the persona
*/
setEmailAddress (email) {
this.email = email
}
/**
* Set the URL to the persona's avatar
* @function
* @param {string} avatarUrl - URL to the persona's avatar
*/
setAvatarUrl (avatarUrl) {
this.avatarUrl = avatarUrl
}
/**
* Add a claim
* @function
* @param {Claim} claim - Claim to add
*/
addClaim (claim) {
this.claims.push(claim)
}
/**
* Revoke the persona
* @function
*/
revoke () {
this.isRevoked = true
}
/**
* Get a JSON representation of the persona
* @function
* @returns {object} JSON representation of the persona
*/
toJSON () {
return {
identifier: this.identifier,
name: this.name,
email: this.email,
description: this.description,
avatarUrl: this.avatarUrl,
themeColor: this.themeColor,
isRevoked: this.isRevoked,
claims: this.claims.map(x => x.toJSON())
}
}
}
/**
* @ignore
* @param {object} personaObject - JSON representation of a persona
* @returns {Persona | Error} Parsed persona
*/
function importJsonPersonaVersion2 (personaObject) {
const claims = personaObject.claims.map(x => Claim.fromJSON(x))
const persona = new Persona(personaObject.name, claims)
persona.identifier = personaObject.identifier
persona.email = personaObject.email
persona.description = personaObject.description
persona.avatarUrl = personaObject.avatarUrl
persona.themeColor = personaObject.avatarUrl
persona.isRevoked = personaObject.isRevoked
return persona
}
module.exports = Persona

View file

@ -13,41 +13,27 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { PublicKeyFetchMethod, PublicKeyEncoding, PublicKeyType, ProfileType } from './enums.js'
import { Persona } from './persona.js'
// eslint-disable-next-line
const Persona = require('./persona')
/**
* @class
* @classdesc A profile of personas with identity claims
* @param {Array<Persona>} personas - Personas of the profile
* A profile of personas with identity claims
* @function
* @param {Array<Persona>} personas
* @public
* @example
* const claim = Claim('https://alice.tld', '123');
* const pers = Persona('Alice', 'About Alice', [claim]);
* const profile = Profile([pers]);
*/
export class Profile {
class Profile {
/**
* Create a new profile
* @function
* @param {ProfileType} profileType - Type of profile (ASP, OpenPGP, etc.)
* @param {string} identifier - Profile identifier (fingerprint, URI, etc.)
* @param {Array<Persona>} personas - Personas of the profile
* @param {Array<Persona>} personas
* @public
*/
constructor (profileType, identifier, personas) {
this.profileVersion = 2
/**
* Profile version
* @type {ProfileType}
* @public
*/
this.profileType = profileType
/**
* Identifier of the profile (fingerprint, email address, uri...)
* @type {string}
* @public
*/
this.identifier = identifier
constructor (personas) {
/**
* List of personas
* @type {Array<Persona>}
@ -56,121 +42,11 @@ export class Profile {
this.personas = personas || []
/**
* Index of primary persona (to be displayed first or prominently)
* @type {number}
* @type {Number}
* @public
*/
this.primaryPersonaIndex = personas.length > 0 ? 0 : -1
/**
* The cryptographic key associated with the profile
* @type {import('./types').ProfilePublicKey}
* @public
*/
this.publicKey = {
keyType: PublicKeyType.NONE,
fingerprint: null,
encoding: PublicKeyEncoding.NONE,
encodedKey: null,
key: null,
fetch: {
method: PublicKeyFetchMethod.NONE,
query: null,
resolvedUrl: null
}
}
/**
* List of verifier URLs
* @type {Array<import('./types').ProfileVerifier>}
* @public
*/
this.verifiers = []
}
/**
* Parse a JSON object and convert it into a profile
* @function
* @param {object} profileObject - JSON representation of a profile
* @returns {Profile | Error} Parsed profile
* @example
* doip.Profile.fromJSON(JSON.stringify(profile));
*/
static fromJSON (profileObject) {
/** @type {Profile} */
let profile
let result
if (typeof profileObject === 'object' && 'profileVersion' in profileObject) {
switch (profileObject.profileVersion) {
case 2:
result = importJsonProfileVersion2(profileObject)
if (result instanceof Error) {
throw result
}
profile = result
break
default:
throw new Error('Invalid profile version')
}
}
return profile
}
/**
* Add profile verifier to the profile
* @function
* @param {string} name - Name of the verifier
* @param {string} url - URL of the verifier
*/
addVerifier (name, url) {
this.verifiers.push({ name, url })
}
/**
* Get a JSON representation of the profile
* @function
* @returns {object} JSON representation of the profile
*/
toJSON () {
return {
profileVersion: this.profileVersion,
profileType: this.profileType,
identifier: this.identifier,
personas: this.personas.map(x => x.toJSON()),
primaryPersonaIndex: this.primaryPersonaIndex,
publicKey: {
keyType: this.publicKey.keyType,
fingerprint: this.publicKey.fingerprint,
encoding: this.publicKey.encoding,
encodedKey: this.publicKey.encodedKey,
fetch: {
method: this.publicKey.fetch.method,
query: this.publicKey.fetch.query,
resolvedUrl: this.publicKey.fetch.resolvedUrl
}
},
verifiers: this.verifiers
}
this.primaryPersona = -1
}
}
/**
* @ignore
* @param {object} profileObject - JSON representation of the profile
* @returns {Profile | Error} Parsed profile
*/
function importJsonProfileVersion2 (profileObject) {
if (!('profileVersion' in profileObject && profileObject.profileVersion === 2)) {
return new Error('Invalid profile')
}
const personas = profileObject.personas.map(x => Persona.fromJSON(x, 2))
const profile = new Profile(profileObject.profileType, profileObject.identifier, personas)
profile.primaryPersonaIndex = profileObject.primaryPersonaIndex
profile.publicKey = profileObject.publicKey
profile.verifiers = profileObject.verifiers
return profile
}
module.exports = Profile

View file

@ -13,11 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { isNode } from 'browser-or-node'
import { fetcher } from './index.js'
import { generateProxyURL } from './utils.js'
import { ProxyPolicy, ProofAccessRestriction } from './enums.js'
import { ServiceProvider } from './serviceProvider.js'
const jsEnv = require('browser-or-node')
const fetcher = require('./fetcher')
const utils = require('./utils')
const E = require('./enums')
/**
* @module proofs
@ -29,35 +28,40 @@ import { ServiceProvider } from './serviceProvider.js'
* the `data` parameter and the proxy policy set in the `opts` parameter to
* choose the right approach to fetch the proof. An error will be thrown if no
* approach is possible.
* @param {ServiceProvider} data - Data from a claim definition
* @param {import('./types').VerificationConfig} opts - Options to enable the request
* @returns {Promise<object|string>} Fetched proof data
* @async
* @param {object} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>}
*/
export async function fetch (data, opts) {
if (isNode) {
const fetch = (data, opts) => {
switch (data.proof.request.fetcher) {
case E.Fetcher.HTTP:
data.proof.request.data.format = data.proof.request.format
break
default:
break
}
if (jsEnv.isNode) {
return handleNodeRequests(data, opts)
}
return handleBrowserRequests(data, opts)
}
/**
* @param {ServiceProvider} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>} Fetched proof data
*/
const handleBrowserRequests = (data, opts) => {
switch (opts.proxy.policy) {
case ProxyPolicy.ALWAYS:
case E.ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts)
case ProxyPolicy.NEVER:
switch (data.proof.request.accessRestriction) {
case ProofAccessRestriction.NONE:
case ProofAccessRestriction.GRANTED:
case E.ProxyPolicy.NEVER:
switch (data.proof.request.access) {
case E.ProofAccess.GENERIC:
case E.ProofAccess.GRANTED:
return createDefaultRequestPromise(data, opts)
case ProofAccessRestriction.NOCORS:
case ProofAccessRestriction.SERVER:
case E.ProofAccess.NOCORS:
case E.ProofAccess.SERVER:
throw new Error(
'Impossible to fetch proof (bad combination of service access and proxy policy)'
)
@ -65,15 +69,15 @@ const handleBrowserRequests = (data, opts) => {
throw new Error('Invalid proof access value')
}
case ProxyPolicy.ADAPTIVE:
switch (data.proof.request.accessRestriction) {
case ProofAccessRestriction.NONE:
case E.ProxyPolicy.ADAPTIVE:
switch (data.proof.request.access) {
case E.ProofAccess.GENERIC:
return createFallbackRequestPromise(data, opts)
case ProofAccessRestriction.NOCORS:
case E.ProofAccess.NOCORS:
return createProxyRequestPromise(data, opts)
case ProofAccessRestriction.GRANTED:
case E.ProofAccess.GRANTED:
return createFallbackRequestPromise(data, opts)
case ProofAccessRestriction.SERVER:
case E.ProofAccess.SERVER:
return createProxyRequestPromise(data, opts)
default:
throw new Error('Invalid proof access value')
@ -84,20 +88,15 @@ const handleBrowserRequests = (data, opts) => {
}
}
/**
* @param {ServiceProvider} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>} Fetched proof data
*/
const handleNodeRequests = (data, opts) => {
switch (opts.proxy.policy) {
case ProxyPolicy.ALWAYS:
case E.ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts)
case ProxyPolicy.NEVER:
case E.ProxyPolicy.NEVER:
return createDefaultRequestPromise(data, opts)
case ProxyPolicy.ADAPTIVE:
case E.ProxyPolicy.ADAPTIVE:
return createFallbackRequestPromise(data, opts)
default:
@ -105,16 +104,8 @@ const handleNodeRequests = (data, opts) => {
}
}
/**
* @param {ServiceProvider} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>} Fetched proof data
*/
const createDefaultRequestPromise = (data, opts) => {
return new Promise((resolve, reject) => {
if (!(data.proof.request.fetcher in fetcher)) {
reject(new Error(`fetcher for ${data.proof.request.fetcher} not found`))
}
fetcher[data.proof.request.fetcher]
.fn(data.proof.request.data, opts)
.then((res) => {
@ -131,16 +122,11 @@ const createDefaultRequestPromise = (data, opts) => {
})
}
/**
* @param {ServiceProvider} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>} Fetched proof data
*/
const createProxyRequestPromise = (data, opts) => {
return new Promise((resolve, reject) => {
let proxyUrl
try {
proxyUrl = generateProxyURL(
proxyUrl = utils.generateProxyURL(
data.proof.request.fetcher,
data.proof.request.data,
opts
@ -151,8 +137,8 @@ const createProxyRequestPromise = (data, opts) => {
const requestData = {
url: proxyUrl,
format: data.proof.response.format,
fetcherTimeout: data.proof.request.fetcher in fetcher ? fetcher[data.proof.request.fetcher].timeout : 30000
format: data.proof.request.format,
fetcherTimeout: fetcher[data.proof.request.fetcher].timeout
}
fetcher.http
.fn(requestData, opts)
@ -170,11 +156,6 @@ const createProxyRequestPromise = (data, opts) => {
})
}
/**
* @param {ServiceProvider} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>} Fetched proof data
*/
const createFallbackRequestPromise = (data, opts) => {
return new Promise((resolve, reject) => {
createDefaultRequestPromise(data, opts)
@ -192,3 +173,5 @@ const createFallbackRequestPromise = (data, opts) => {
})
})
}
exports.fetch = fetch

View file

@ -1,373 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const profile = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/profile.schema.json',
title: 'Profile',
description: 'Keyoxide profile with personas',
type: 'object',
properties: {
profileVersion: {
description: 'The version of the profile',
type: 'integer'
},
profileType: {
description: 'The type of the profile [openpgp, asp]',
type: 'string'
},
identifier: {
description: 'Identifier of the profile (email, fingerprint, URI)',
type: 'string'
},
personas: {
description: 'The personas inside the profile',
type: 'array',
items: {
$ref: 'https://spec.keyoxide.org/2/persona.schema.json'
},
minItems: 1,
uniqueItems: true
},
primaryPersonaIndex: {
description: 'The index of the primary persona',
type: 'integer'
},
publicKey: {
description: 'The cryptographic key associated with the profile',
type: 'object',
properties: {
keyType: {
description: 'The type of cryptographic key [eddsa, es256, openpgp, none]',
type: 'string'
},
encoding: {
description: 'The encoding of the cryptographic key [pem, jwk, armored_pgp, none]',
type: 'string'
},
encodedKey: {
description: 'The encoded cryptographic key (PEM, stringified JWK, ...)',
type: ['string', 'null']
},
fetch: {
description: 'Details on how to fetch the public key',
type: 'object',
properties: {
method: {
description: 'The method to fetch the key [aspe, hkp, wkd, http, none]',
type: 'string'
},
query: {
description: 'The query to fetch the key',
type: ['string', 'null']
},
resolvedUrl: {
description: 'The URL the method eventually resolved to',
type: ['string', 'null']
}
}
}
},
required: [
'keyType',
'fetch'
]
},
verifiers: {
description: 'A list of links to verifiers',
type: 'array',
items: {
type: 'object',
properties: {
name: {
description: 'Name of the verifier site',
type: 'string'
},
url: {
description: 'URL to the profile page on the verifier site',
type: 'string'
}
}
},
uniqueItems: true
}
},
required: [
'profileVersion',
'profileType',
'identifier',
'personas',
'primaryPersonaIndex',
'publicKey',
'verifiers'
],
additionalProperties: false
}
export const persona = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/persona.schema.json',
title: 'Profile',
description: 'Keyoxide persona with identity claims',
type: 'object',
properties: {
identifier: {
description: 'Identifier of the persona',
type: ['string', 'null']
},
name: {
description: 'Name of the persona',
type: 'string'
},
email: {
description: 'Email address of the persona',
type: ['string', 'null']
},
description: {
description: 'Description of the persona',
type: ['string', 'null']
},
avatarUrl: {
description: 'URL to an avatar image',
type: ['string', 'null']
},
themeColor: {
description: 'Profile page theme color',
type: ['string', 'null']
},
isRevoked: {
type: 'boolean'
},
claims: {
description: 'A list of identity claims',
type: 'array',
items: {
$ref: 'https://spec.keyoxide.org/2/claim.schema.json'
},
uniqueItems: true
}
},
required: [
'name',
'claims'
],
additionalProperties: false
}
export const claim = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/claim.schema.json',
title: 'Identity claim',
description: 'Verifiable online identity claim',
type: 'object',
properties: {
claimVersion: {
description: 'The version of the claim',
type: 'integer'
},
uri: {
description: 'The claim URI',
type: 'string'
},
proofs: {
description: 'The proofs that would verify the claim',
type: 'array',
items: {
type: 'string'
},
minItems: 1,
uniqueItems: true
},
matches: {
description: 'Service providers matched to the claim',
type: 'array',
items: {
$ref: 'https://spec.keyoxide.org/2/serviceprovider.schema.json'
},
uniqueItems: true
},
status: {
type: 'integer',
description: 'Claim status code'
},
display: {
type: 'object',
properties: {
profileName: {
type: 'string',
description: 'Account name to display in the user interface'
},
profileUrl: {
type: ['string', 'null'],
description: 'Profile URL to link to in the user interface'
},
proofUrl: {
type: ['string', 'null'],
description: 'Proof URL to link to in the user interface'
},
serviceProviderName: {
type: ['string', 'null'],
description: 'Name of the service provider to display in the user interface'
},
serviceProviderId: {
type: ['string', 'null'],
description: 'Id of the service provider'
}
}
}
},
required: [
'claimVersion',
'uri',
'proofs',
'status',
'display'
],
additionalProperties: false
}
export const serviceprovider = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/serviceprovider.schema.json',
title: 'Service provider',
description: 'A service provider that can be matched to identity claims',
type: 'object',
properties: {
about: {
description: 'Details about the service provider',
type: 'object',
properties: {
name: {
description: 'Full name of the service provider',
type: 'string'
},
id: {
description: 'Identifier of the service provider (no whitespace or symbols, lowercase)',
type: 'string'
},
homepage: {
description: 'URL to the homepage of the service provider',
type: ['string', 'null']
}
}
},
profile: {
description: 'What the profile would look like if the match is correct',
type: 'object',
properties: {
display: {
description: 'Profile name to be displayed',
type: 'string'
},
uri: {
description: 'URI or URL for public access to the profile',
type: 'string'
},
qr: {
description: 'URI or URL associated with the profile usually served as a QR code',
type: ['string', 'null']
}
}
},
claim: {
description: 'Details from the claim matching process',
type: 'object',
properties: {
uriRegularExpression: {
description: 'Regular expression used to parse the URI',
type: 'string'
},
uriIsAmbiguous: {
description: 'Whether this match automatically excludes other matches',
type: 'boolean'
}
}
},
proof: {
description: 'Information for the proof verification process',
type: 'object',
properties: {
request: {
description: 'Details to request the potential proof',
type: 'object',
properties: {
uri: {
description: 'Location of the proof',
type: ['string', 'null']
},
accessRestriction: {
description: 'Type of access restriction [none, nocors, granted, server]',
type: 'string'
},
fetcher: {
description: 'Name of the fetcher to use',
type: 'string'
},
data: {
description: 'Data needed by the fetcher or proxy to request the proof',
type: 'object',
additionalProperties: true
}
}
},
response: {
description: 'Details about the expected response',
type: 'object',
properties: {
format: {
description: 'Expected format of the proof [text, json]',
type: 'string'
}
}
},
target: {
description: 'Details about the target located in the response',
type: 'array',
items: {
type: 'object',
properties: {
format: {
description: 'How is the proof formatted [uri, fingerprint]',
type: 'string'
},
encoding: {
description: 'How is the proof encoded [plain, html, xml]',
type: 'string'
},
relation: {
description: 'How are the response and the target related [contains, equals]',
type: 'string'
},
path: {
description: 'Path to the target location if the response is JSON',
type: 'array',
items: {
type: 'string'
}
}
}
}
}
}
}
},
required: [
'about',
'profile',
'claim',
'proof'
],
additionalProperties: false
}

View file

@ -1,62 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* A service provider matched to an identity claim
* @class
* @public
*/
export class ServiceProvider {
/**
* @param {import('./types').ServiceProviderObject} serviceProviderObject - JSON representation of a {@link ServiceProvider}
*/
constructor (serviceProviderObject) {
/**
* Details about the service provider
* @type {import('./types').ServiceProviderAbout}
*/
this.about = serviceProviderObject.about
/**
* What the profile would look like if a claim matches this service provider
* @type {import('./types').ServiceProviderProfile}
*/
this.profile = serviceProviderObject.profile
/**
* Information about the claim matching process
* @type {import('./types').ServiceProviderClaim}
*/
this.claim = serviceProviderObject.claim
/**
* Information for the proof verification process
* @type {import('./types').ServiceProviderProof}
*/
this.proof = serviceProviderObject.proof
}
/**
* Get a JSON representation of the {@link ServiceProvider}
* @function
* @returns {import('./types').ServiceProviderObject} JSON representation of a {@link ServiceProvider}
*/
toJSON () {
return {
about: this.about,
profile: this.profile,
claim: this.claim,
proof: this.proof
}
}
}

View file

@ -1,270 +0,0 @@
/*
Copyright 2022 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* ActivityPub service provider ({@link https://docs.keyoxide.org/service-providers/activitypub/|Keyoxide docs})
* @module serviceProviders/activitypub
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.activitypub.processURI('https://domain.example/@alice');
*/
import * as E from '../enums.js'
import { fetcher } from '../index.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
return new ServiceProvider({
about: {
id: 'activitypub',
name: 'ActivityPub',
homepage: 'https://activitypub.rocks'
},
profile: {
display: uri,
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString().toString(),
uriIsAmbiguous: true
},
proof: {
request: {
uri,
fetcher: E.Fetcher.ACTIVITYPUB,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: uri
}
},
response: {
format: E.ProofFormat.JSON
},
target: [
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['summary']
},
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['attachment', 'value']
},
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['content']
}
]
}
})
}
export const functions = {
postprocess: async (/** @type {ServiceProvider} */ claimData, proofData, opts) => {
switch (proofData.result.type) {
case 'Note': {
claimData.profile.uri = proofData.result.attributedTo
claimData.profile.display = proofData.result.attributedTo
const personData = await fetcher.activitypub.fn({ url: proofData.result.attributedTo }, opts)
.catch(_ => null)
if (personData) {
claimData.profile.display = `@${personData.preferredUsername}@${new URL(claimData.proof.request.uri).hostname}`
}
break
}
case 'Person':
claimData.profile.display = `@${proofData.result.preferredUsername}@${new URL(claimData.proof.request.uri).hostname}`
break
default:
break
}
// Attempt to fetch and process the instance's NodeInfo data
const nodeinfo = await _processNodeinfo(new URL(claimData.proof.request.uri).hostname)
if (nodeinfo) {
claimData.about.name = nodeinfo.software.name
claimData.about.id = nodeinfo.software.name
claimData.about.homepage = nodeinfo.software.homepage
}
return { claimData, proofData }
}
}
const _processNodeinfo = async (/** @type {string} */ domain) => {
const nodeinfoRef = await fetch(`https://${domain}/.well-known/nodeinfo`)
.then(res => {
if (res.status !== 200) {
throw new Error('HTTP Status was not 200')
}
return res.json()
})
.catch(_ => {
return null
})
if (!nodeinfoRef) return null
// NodeInfo version 2.1
{
const nodeinfo = nodeinfoRef.links.find(x => { return x.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1' })
if (nodeinfo) {
return await fetch(nodeinfo.href)
.then(res => {
if (res.status !== 200) {
throw new Error('HTTP Status was not 200')
}
return res.json()
})
.then(res => {
return {
software: {
name: res.software.name,
version: res.software.version,
homepage: res.software.homepage || 'https://activitypub.rocks'
}
}
})
.catch(_ => {
return null
})
}
}
// NodeInfo version 2.0
{
const nodeinfo = nodeinfoRef.links.find(x => { return x.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0' })
if (nodeinfo) {
return await fetch(nodeinfo.href)
.then(res => {
if (res.status !== 200) {
throw new Error('HTTP Status was not 200')
}
return res.json()
})
.then(res => {
return {
software: {
name: res.software.name,
version: res.software.version,
homepage: 'https://activitypub.rocks'
}
}
})
.catch(_ => {
return null
})
}
}
// NodeInfo version 1.1
{
const nodeinfo = nodeinfoRef.links.find(x => { return x.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.1' })
if (nodeinfo) {
return await fetch(nodeinfo.href)
.then(res => {
if (res.status !== 200) {
throw new Error('HTTP Status was not 200')
}
return res.json()
})
.then(res => {
return {
software: {
name: res.software.name,
version: res.software.version,
homepage: 'https://activitypub.rocks'
}
}
})
.catch(_ => {
return null
})
}
}
// NodeInfo version 1.0
{
const nodeinfo = nodeinfoRef.links.find(x => { return x.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0' })
if (nodeinfo) {
return await fetch(nodeinfo.href)
.then(res => {
if (res.status !== 200) {
throw new Error('HTTP Status was not 200')
}
return res.json()
})
.then(res => {
return {
software: {
name: res.software.name,
version: res.software.version,
homepage: 'https://activitypub.rocks'
}
}
})
.catch(_ => {
return null
})
}
}
}
export const tests = [
{
uri: 'https://domain.org',
shouldMatch: true
},
{
uri: 'https://domain.org/@/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/@alice',
shouldMatch: true
},
{
uri: 'https://domain.org/@alice/123456',
shouldMatch: true
},
{
uri: 'https://domain.org/u/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/users/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/users/alice/123456',
shouldMatch: true
},
{
uri: 'http://domain.org/alice',
shouldMatch: false
}
]

View file

@ -1,95 +0,0 @@
/*
Copyright 2024 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* ASPE service provider ({@link https://docs.keyoxide.org/service-providers/aspe/|Keyoxide docs})
* @module serviceProviders/aspe
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.activitypub.processURI('aspe:domain.example:abc123def456');
*/
import isFQDN from 'validator/lib/isFQDN.js'
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^aspe:([a-zA-Z0-9.\-_]*):([a-zA-Z0-9]*)/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
if (!isFQDN(match[1])) {
return null
}
return new ServiceProvider({
about: {
id: 'aspe',
name: 'ASPE'
},
profile: {
display: uri,
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: null,
fetcher: E.Fetcher.ASPE,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
aspeUri: uri
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['claims']
}]
}
})
}
export const tests = [
{
uri: 'aspe:domain.tld:abc123def456',
shouldMatch: true
},
{
uri: 'aspe:domain.tld',
shouldMatch: false
},
{
uri: 'dns:domain.tld',
shouldMatch: false
},
{
uri: 'https://domain.tld',
shouldMatch: false
}
]

View file

@ -1,127 +0,0 @@
/*
Copyright 2024 Bram Hagens
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Discord service provider
* @module serviceProviders/discord
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.discord.processURI('https://discord.com/invite/AbCdEf');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(?:discord\.gg|discord\.com\/invite)\/(.+)/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'discord',
name: 'Discord',
homepage: 'https://discord.com'
},
profile: {
display: null,
uri: null,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
// Get proof from invites (https://discord.com/developers/docs/resources/invite#get-invite)
// See https://discord.com/developers/docs/reference#api-versioning for Discord's API versioning
proof: {
request: {
uri: `https://discord.com/api/v10/invites/${match[1]}`,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://discord.com/api/v10/invites/${match[1]}`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['guild', 'description']
},
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['guild', 'name']
}
]
}
})
}
export const functions = {
postprocess: async (claimData, proofData, opts) => {
// Extract inviter's username from https://discord.com/developers/docs/resources/invite#invite-object
claimData.profile.display = proofData.result.inviter.username
return { claimData, proofData }
}
}
export const tests = [
{
uri: 'https://discord.com/invite/AbCdEf',
shouldMatch: true
},
{
uri: 'https://discord.com/invite/AbCdEfGh',
shouldMatch: true
},
{
uri: 'https://discord.gg/AbCdEf',
shouldMatch: true
},
{
uri: 'https://discord.gg/AbCdEfGh',
shouldMatch: true
},
{
uri: 'https://domain.com/invite/AbCdEf',
shouldMatch: false
},
{
uri: 'https://domain.gg/AbCdEf',
shouldMatch: false
},
{
uri: 'https://discord.com/invite/',
shouldMatch: false
},
{
uri: 'https://discord.gg/',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Discourse service provider ({@link https://docs.keyoxide.org/service-providers/discourse/|Keyoxide docs})
* @module serviceProviders/discourse
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.activitypub.processURI('https://domain.example/u/alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*)\/u\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'discourse',
name: 'Discourse',
homepage: 'https://www.discourse.org'
},
profile: {
display: `${match[2]}@${match[1]}`,
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString().toString(),
uriIsAmbiguous: true
},
proof: {
request: {
uri,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://${match[1]}/u/${match[2]}.json`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['user', 'bio_raw']
}]
}
})
}
export const tests = [
{
uri: 'https://domain.org/u/alice',
shouldMatch: true
},
{
uri: 'https://domain.org/u/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]

View file

@ -1,86 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* DNS service provider ({@link https://docs.keyoxide.org/service-providers/dns/|Keyoxide docs})
* @module serviceProviders/dns
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.dns.processURI('dns:domain.example?type=TXT');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^dns:([a-zA-Z0-9.\-_]*)(?:\?(.*))?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'dns',
name: 'DNS'
},
profile: {
display: match[1],
uri: `https://${match[1]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: null,
fetcher: E.Fetcher.DNS,
accessRestriction: E.ProofAccessRestriction.SERVER,
data: {
domain: match[1]
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['records', 'txt']
}]
}
})
}
export const tests = [
{
uri: 'dns:domain.org',
shouldMatch: true
},
{
uri: 'dns:domain.org?type=TXT',
shouldMatch: true
},
{
uri: 'https://domain.org',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Forem service provider ({@link https://docs.keyoxide.org/service-providers/forem/|Keyoxide docs})
* @module serviceProviders/forem
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.forem.processURI('https://domain.example/alice/title');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*)\/(.*)\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'forem',
name: 'Forem',
homepage: 'https://www.forem.com'
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString().toString(),
uriIsAmbiguous: true
},
proof: {
request: {
uri,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://${match[1]}/api/articles/${match[2]}/${match[3]}`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['body_markdown']
}]
}
})
}
export const tests = [
{
uri: 'https://domain.org/alice/post',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/post/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]

View file

@ -1,101 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Forgejo service provider ({@link https://docs.keyoxide.org/service-providers/forgejo/|Keyoxide docs})
* @module serviceProviders/forgejo
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.forgejo.processURI('https://domain.example/alice/repo');
*/
import * as E from '../enums.js'
import { fetcher } from '../index.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*)\/(.*)\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'forgejo',
name: 'Forgejo',
homepage: 'https://forgejo.org'
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: true
},
proof: {
request: {
uri,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://${match[1]}/api/v1/repos/${match[2]}/${match[3]}`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}]
}
})
}
export const functions = {
validate: async (/** @type {ServiceProvider} */ claimData, proofData, opts) => {
const url = `https://${new URL(claimData.proof.request.uri).hostname}/api/forgejo/v1/version`
const forgejoData = await fetcher.http.fn({ url, format: E.ProofFormat.JSON }, opts)
return forgejoData && 'version' in forgejoData
}
}
export const tests = [
{
uri: 'https://domain.org/alice/forgejo_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/forgejo_proof/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/other_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]

View file

@ -1,96 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Github service provider ({@link https://docs.keyoxide.org/service-providers/github/|Keyoxide docs})
* @module serviceProviders/github
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.github.processURI('https://gist.github.com/alice/title');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/gist\.github\.com\/(.*)\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'github',
name: 'GitHub',
homepage: 'https://github.com'
},
profile: {
display: match[1],
uri: `https://github.com/${match[1]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `https://api.github.com/gists/${match[2]}`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['files', 'proof.md', 'content']
},
{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['files', 'openpgp.md', 'content']
}
]
}
})
}
export const tests = [
{
uri: 'https://gist.github.com/Alice/123456789',
shouldMatch: true
},
{
uri: 'https://gist.github.com/Alice/123456789/',
shouldMatch: true
},
{
uri: 'https://domain.org/Alice/123456789',
shouldMatch: false
}
]

View file

@ -1,87 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Gitlab service provider ({@link https://docs.keyoxide.org/service-providers/gitlab/|Keyoxide docs})
* @module serviceProviders/gitlab
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.gitlab.processURI('https://domain.example/alice/repo');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*)\/(.*)\/gitlab_proof\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'gitlab',
name: 'GitLab',
homepage: 'https://about.gitlab.com'
},
profile: {
display: `${match[2]}@${match[1]}`,
uri: `https://${match[1]}/${match[2]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: true
},
proof: {
request: {
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `https://${match[1]}/api/v4/projects/${match[2]}%2Fgitlab_proof`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}]
}
})
}
export const tests = [
{
uri: 'https://gitlab.domain.org/alice/gitlab_proof',
shouldMatch: true
},
{
uri: 'https://gitlab.domain.org/alice/gitlab_proof/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/other_proof',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Hackernews service provider ({@link https://docs.keyoxide.org/service-providers/hackernews/|Keyoxide docs})
* @module serviceProviders/hackernews
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.hackernews.processURI('https://news.ycombinator.com/user?id=alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/news\.ycombinator\.com\/user\?id=(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'hackernews',
name: 'Hacker News',
homepage: 'https://news.ycombinator.com'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.HTML,
relation: E.ClaimRelation.CONTAINS,
path: ['about']
}]
}
})
}
export const tests = [
{
uri: 'https://news.ycombinator.com/user?id=Alice',
shouldMatch: true
},
{
uri: 'https://news.ycombinator.com/user?id=Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/user?id=Alice',
shouldMatch: false
}
]

View file

@ -1,75 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as aspe from './aspe.js'
import * as openpgp from './openpgp.js'
import * as dns from './dns.js'
import * as irc from './irc.js'
import * as xmpp from './xmpp.js'
import * as matrix from './matrix.js'
import * as telegram from './telegram.js'
import * as twitter from './twitter.js'
import * as reddit from './reddit.js'
import * as liberapay from './liberapay.js'
import * as lichess from './lichess.js'
import * as hackernews from './hackernews.js'
import * as lobsters from './lobsters.js'
import * as forem from './forem.js'
import * as forgejo from './forgejo.js'
import * as gitea from './gitea.js'
import * as gitlab from './gitlab.js'
import * as github from './github.js'
import * as activitypub from './activitypub.js'
import * as discourse from './discourse.js'
import * as owncast from './owncast.js'
import * as stackexchange from './stackexchange.js'
import * as keybase from './keybase.js'
import * as opencollective from './opencollective.js'
import * as orcid from './orcid.js'
import * as pronounscc from './pronounscc.js'
import * as discord from './discord.js'
const _data = {
aspe,
openpgp,
dns,
irc,
xmpp,
matrix,
telegram,
twitter,
reddit,
liberapay,
lichess,
hackernews,
lobsters,
forem,
forgejo,
gitea,
gitlab,
github,
activitypub,
discourse,
owncast,
stackexchange,
keybase,
opencollective,
orcid,
pronounscc,
discord
}
export const list = Object.keys(_data)
export { _data as data }

View file

@ -1,91 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* IRC service provider ({@link https://docs.keyoxide.org/service-providers/irc/|Keyoxide docs})
* @module serviceProviders/irc
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.irc.processURI('irc://domain.example/alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^irc:\/\/(.*)\/([a-zA-Z0-9\-[\]\\`_^{|}]*)/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'irc',
name: 'IRC'
},
profile: {
display: `${match[1]}/${match[2]}`,
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: null,
fetcher: E.Fetcher.IRC,
accessRestriction: E.ProofAccessRestriction.SERVER,
data: {
domain: match[1],
nick: match[2]
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: []
}]
}
})
}
export const tests = [
{
uri: 'irc://chat.ircserver.org/Alice1',
shouldMatch: true
},
{
uri: 'irc://chat.ircserver.org/alice?param=123',
shouldMatch: true
},
{
uri: 'irc://chat.ircserver.org/alice_bob',
shouldMatch: true
},
{
uri: 'https://chat.ircserver.org/alice',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Keybase service provider ({@link https://docs.keyoxide.org/service-providers/keybase/|Keyoxide docs})
* @module serviceProviders/keybase
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.keybase.processURI('https://keybase.io/alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/keybase.io\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'keybase',
name: 'keybase',
homepage: 'https://keybase.io'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: `https://keybase.io/_/api/1.0/user/lookup.json?username=${match[1]}`,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://keybase.io/_/api/1.0/user/lookup.json?username=${match[1]}`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['them', 'public_keys', 'primary', 'key_fingerprint']
}]
}
})
}
export const tests = [
{
uri: 'https://keybase.io/Alice',
shouldMatch: true
},
{
uri: 'https://keybase.io/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/Alice',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Liberapay service provider ({@link https://docs.keyoxide.org/service-providers/liberapay/|Keyoxide docs})
* @module serviceProviders/liberapay
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.liberapay.processURI('https://liberapay.com/alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/liberapay\.com\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'liberapay',
name: 'Liberapay',
homepage: 'https://liberapay.com'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `https://liberapay.com/${match[1]}/public.json`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['statements', 'content']
}]
}
})
}
export const tests = [
{
uri: 'https://liberapay.com/alice',
shouldMatch: true
},
{
uri: 'https://liberapay.com/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Lichess service provider ({@link https://docs.keyoxide.org/service-providers/lichess/|Keyoxide docs})
* @module serviceProviders/lichess
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.lichess.processURI('https://lichess.org/@/alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/lichess\.org\/@\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'lichess',
name: 'Lichess',
homepage: 'https://lichess.org'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: `https://lichess.org/api/user/${match[1]}`,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `https://lichess.org/api/user/${match[1]}`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['profile', 'links']
}]
}
})
}
export const tests = [
{
uri: 'https://lichess.org/@/Alice',
shouldMatch: true
},
{
uri: 'https://lichess.org/@/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/@/Alice',
shouldMatch: false
}
]

View file

@ -1,96 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Lobste.rs service provider ({@link https://docs.keyoxide.org/service-providers/lobsters/|Keyoxide docs})
* @module serviceProviders/lobsters
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.lobsters.processURI('https://lobste.rs/~alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/lobste\.rs\/(?:~|u\/)(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'lobsters',
name: 'Lobsters',
homepage: 'https://lobste.rs'
},
profile: {
display: match[1],
uri: `https://lobste.rs/~${match[1]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: `https://lobste.rs/~${match[1]}.json`,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: `https://lobste.rs/~${match[1]}.json`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['about']
}]
}
})
}
export const tests = [
{
uri: 'https://lobste.rs/~Alice',
shouldMatch: true
},
{
uri: 'https://lobste.rs/u/Alice',
shouldMatch: true
},
{
uri: 'https://lobste.rs/u/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/~Alice',
shouldMatch: false
},
{
uri: 'https://domain.org/u/Alice',
shouldMatch: false
}
]

View file

@ -1,88 +0,0 @@
/*
Copyright 2023 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* OpenCollective service provider ({@link https://docs.keyoxide.org/service-providers/opencollective/|Keyoxide docs})
* @module serviceProviders/opencollective
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.opencollective.processURI('https://opencollective.com/alice');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/opencollective\.com\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'opencollective',
name: 'Open Collective',
homepage: 'https://opencollective.com'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri,
fetcher: E.Fetcher.GRAPHQL,
accessRestriction: E.ProofAccessRestriction.NOCORS,
data: {
url: 'https://api.opencollective.com/graphql/v2',
query: `{ "query": "query { account(slug: \\"${match[1]}\\") { longDescription } }" }`
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['data', 'account', 'longDescription']
}]
}
})
}
export const tests = [
{
uri: 'https://opencollective.com/Alice',
shouldMatch: true
},
{
uri: 'https://opencollective.com/Alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/Alice',
shouldMatch: false
}
]

View file

@ -1,179 +0,0 @@
/*
Copyright 2024 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* OpenPGP service provider ({@link https://docs.keyoxide.org/service-providers/openpgp/|Keyoxide docs})
* @module serviceProviders/openpgp
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.openpgp.processURI('openpgp4fpr:ABC123DEF456');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^(.*)/
const reURIHkp = /^openpgp4fpr:(?:0x)?([a-zA-Z0-9.\-_]*)/
const reURIWkdDirect = /^https:\/\/(.*)\/.well-known\/openpgpkey\/hu\/([a-zA-Z0-9]*)(?:\?l=(.*))?/
const reURIWkdAdvanced = /^https:\/\/(openpgpkey.*)\/.well-known\/openpgpkey\/(.*)\/hu\/([a-zA-Z0-9]*)(?:\?l=(.*))?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
let reURI = null
let mode = null
let match = null
if (reURIHkp.test(uri)) {
reURI = reURIHkp
mode = E.OpenPgpQueryProtocol.HKP
match = uri.match(reURI)
}
if (!mode && reURIWkdAdvanced.test(uri)) {
reURI = reURIWkdAdvanced
mode = E.OpenPgpQueryProtocol.WKD
match = uri.match(reURI)
}
if (!mode && reURIWkdDirect.test(uri)) {
reURI = reURIWkdDirect
mode = E.OpenPgpQueryProtocol.WKD
match = uri.match(reURI)
}
let output = null
switch (mode) {
case E.OpenPgpQueryProtocol.HKP:
output = new ServiceProvider({
about: {
id: 'openpgp',
name: 'OpenPGP'
},
profile: {
display: `openpgp4fpr:${match[1]}`,
uri: `https://keys.openpgp.org/search?q=${match[1]}`,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: `https://keys.openpgp.org/vks/v1/by-fingerprint/${match[1].toUpperCase()}`,
fetcher: E.Fetcher.OPENPGP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `https://keys.openpgp.org/vks/v1/by-fingerprint/${match[1].toUpperCase()}`,
protocol: E.OpenPgpQueryProtocol.HKP
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['notations', 'proof@ariadne.id']
}]
}
})
break
case E.OpenPgpQueryProtocol.WKD:
output = new ServiceProvider({
about: {
id: 'openpgp',
name: 'OpenPGP'
},
profile: {
display: 'unknown fingerprint',
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri,
fetcher: E.Fetcher.OPENPGP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: uri,
protocol: E.OpenPgpQueryProtocol.WKD
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['notations', 'proof@ariadne.id']
}]
}
})
break
}
return output
}
export const tests = [
{
uri: 'openpgp4fpr:123456789',
shouldMatch: true
},
{
uri: 'openpgp4fpr:abcdef123',
shouldMatch: true
},
{
uri: 'https://openpgpkey.domain.tld/.well-known/openpgpkey/domain.tld/hu/123abc456def?l=name',
shouldMatch: true
},
{
uri: 'https://openpgpkey.domain.tld/.well-known/openpgpkey/domain.tld/hu/123abc456def',
shouldMatch: true
},
{
uri: 'https://domain.tld/.well-known/openpgpkey/hu/123abc456def?l=name',
shouldMatch: true
},
{
uri: 'https://domain.tld/.well-known/openpgpkey/hu/123abc456def',
shouldMatch: true
},
// The following will not pass .processURI, but reURI currently accepts anything
{
uri: 'https://domain.tld',
shouldMatch: true
},
{
uri: 'https://openpgpkey.domain.tld/.well-known/openpgpkey/hu/123abc456def?l=name',
shouldMatch: true
},
{
uri: 'https://domain.tld/.well-known/openpgpkey/123abc456def?l=name',
shouldMatch: true
}
]

View file

@ -1,98 +0,0 @@
/*
Copyright 2023 Tim Haase
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* ORCiD service provider ({@link https://docs.keyoxide.org/service-providers/orcid/|Keyoxide docs})
* @module serviceProviders/orcid
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.orcid.processURI('https://orcid.org/123-456-789-123');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/orcid\.org\/(.*)\/?/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'orcid',
name: 'ORCiD',
homepage: 'https://orcid.org/'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: uri,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['person', 'biography', 'content']
}, {
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['person', 'researcher-urls', 'researcher-url', 'url', 'value']
}, {
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['person', 'keywords', 'keyword', 'content']
}]
}
})
}
export const tests = [
{
uri: 'https://orcid.org/0000-0000-0000-0000',
shouldMatch: true
},
{
uri: 'https://orcid.org/0000-0000-0000-0000/',
shouldMatch: true
},
{
uri: 'https://domain.org/0000-0000-0000-0000',
shouldMatch: false
}
]

View file

@ -1,92 +0,0 @@
/*
Copyright 2021 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Owncast service provider ({@link https://docs.keyoxide.org/service-providers/owncast/|Keyoxide docs})
* @module serviceProviders/owncast
* @example
* import { ServiceProviderDefinitions } from 'doipjs';
* const sp = ServiceProviderDefinitions.data.owncast.processURI('https://domain.example');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
export const reURI = /^https:\/\/(.*)/
/**
* @function
* @param {string} uri - Claim URI to process
* @returns {ServiceProvider} The service provider information based on the claim URI
*/
export function processURI (uri) {
const match = uri.match(reURI)
return new ServiceProvider({
about: {
id: 'owncast',
name: 'Owncast',
homepage: 'https://owncast.online'
},
profile: {
display: match[1],
uri,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: true
},
proof: {
request: {
uri: `${uri}/api/config`,
fetcher: E.Fetcher.HTTP,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `${uri}/api/config`,
format: E.ProofFormat.JSON
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.FINGERPRINT,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['socialHandles', 'url']
}]
}
})
}
export const tests = [
{
uri: 'https://live.domain.org',
shouldMatch: true
},
{
uri: 'https://live.domain.org/',
shouldMatch: true
},
{
uri: 'https://domain.org/live',
shouldMatch: true
},
{
uri: 'https://domain.org/live/',
shouldMatch: true
}
]

Some files were not shown because too many files have changed in this diff Show more