Compare commits

...

197 commits

Author SHA1 Message Date
Ty
0f43a7b9e3
Update for publishing 2024-06-14 22:50:22 -06:00
Ty
2153185729
Add sasl support 2024-06-14 22:15:42 -06:00
Ty
a523ec4191
Update to yarn modern 2024-06-14 22:15:21 -06:00
André Jaenisch
fa57e7b538 feat: add discord support
bump discord API version to v10

accept discord.gg urls

allow proof in guild name

add comments

Reviewed-on: https://codeberg.org/keyoxide/doipjs/pulls/85
2024-04-08 13:44:44 +00:00
Bram Hagens
758255f652
add comments 2024-04-08 00:22:44 +02:00
Bram Hagens
fff5ce4aca
Merge remote-tracking branch 'upstream/dev' into support-discord 2024-04-08 00:05:52 +02:00
Ty
06ea6732de
Add pronouns.cc verification 2024-03-22 18:44:49 -06:00
Bram Hagens
041e22c52d
allow proof in guild name 2024-02-08 10:36:18 +01:00
Bram Hagens
6d464176df
accept discord.gg urls 2024-02-08 10:36:18 +01:00
Bram Hagens
9b1a5d4d26
bump discord API version to v10 2024-02-08 10:11:52 +01:00
Bram Hagens
689117ac98
feat: add discord support 2024-02-08 01:04:16 +01:00
Yarmo Mackenbach
6d2606c8a9
chore: release 1.2.9 2024-02-01 17:06:40 +01:00
Dario Vladovic
fecaf7df12 feat: remove unused dependencies 2024-01-31 02:47:48 +00:00
Yarmo Mackenbach
c52632cbb6
feat: change jsdoc theme 2024-01-30 01:13:36 +01:00
Yarmo Mackenbach
f8d0422443
fix: review 2024-01-29 14:05:17 +01:00
Yarmo Mackenbach
fe6588dcbb
feat: combine jsdoc with tsimport 2024-01-29 13:19:08 +01:00
Yarmo Mackenbach
deaa858345
feat: improve jsdoc for documentation 2024-01-29 13:19:07 +01:00
Yarmo Mackenbach
336029fd87 fix: fix jsdoc errors found by eslint 2024-01-28 12:55:45 +00:00
Yarmo Mackenbach
3f8579513a feat: add jsdoc plugin to eslint 2024-01-28 12:55:45 +00:00
Yarmo Mackenbach
2ef792fbbb fix: remove TS import types 2024-01-28 12:55:45 +00:00
Yarmo Mackenbach
edc3f401bc
chore: update README [SKIP CI] 2024-01-28 10:20:34 +01:00
Yarmo Mackenbach
7115ecbfe6
fix: fix issue template label [SKIP CI] 2024-01-26 23:09:01 +01:00
Yarmo Mackenbach
d672ea2328
fix: fix issue template label [SKIP CI] 2024-01-26 19:28:36 +01:00
Yarmo Mackenbach
d7d9556165
chore: add issue templates [SKIP CI] 2024-01-26 18:28:27 +01:00
Yarmo Mackenbach
6b8fd0b099 Merge pull request 'support-orcid-claim' (#47) from lepus2589/doipjs:support-orcid-claim into dev
Reviewed-on: https://codeberg.org/keyoxide/doipjs/pulls/47
2024-01-25 21:14:11 +00:00
Tim Haase
7fe9d29690 Add ORCID biography as possible proof location 2024-01-25 21:14:11 +00:00
Tim Haase
0dd0d91104 feat: Add ORCID service provider 2024-01-25 21:14:11 +00:00
Yarmo Mackenbach
626077f880
fix: fix bad reference 2024-01-23 22:55:50 +01:00
Yarmo Mackenbach
7639962521
fix: optimize regexp creation 2024-01-23 20:02:02 +01:00
Yarmo Mackenbach
a9c1ffabfe
fix: move clearTimeout to finally() 2024-01-23 19:58:26 +01:00
Yarmo Mackenbach
c02742e041
chore: release 1.2.8 2024-01-23 19:08:03 +01:00
Yarmo Mackenbach
70ee790f0a Merge pull request 'Support OpenPGP/ASPE claims' (#66) from support-openpgp-aspe-claims into dev
Reviewed-on: https://codeberg.org/keyoxide/doipjs/pulls/66
2024-01-23 17:53:19 +00:00
Yarmo Mackenbach
4a77591c57
feat: support ASPE claims 2024-01-23 15:54:57 +01:00
Yarmo Mackenbach
cd96131ad1
feat: support OpenPGP claims 2024-01-23 13:56:15 +01:00
Yarmo Mackenbach
dd2c134a75
chore: bump deps 2024-01-23 00:27:11 +01:00
Yarmo Mackenbach
7eaaedc12a
chore: remove obsolete file 2024-01-23 00:18:32 +01:00
Yarmo Mackenbach
468f150509
chore: bump deps 2024-01-23 00:14:15 +01:00
Yarmo Mackenbach
462348dced
chore: release 1.2.7 2023-10-09 19:37:29 +02:00
Yarmo Mackenbach
f724e81c06
fix: fix regex errors 2023-10-09 19:34:44 +02:00
Yarmo Mackenbach
9bdc0a639f
fix: fix CI syntax 2023-10-09 19:07:29 +02:00
Yarmo Mackenbach
3c4f61ada1
chore: release 1.2.6 2023-10-09 16:28:23 +02:00
Yarmo Mackenbach
2687742e23
feat: add github proof location 2023-10-09 16:21:06 +02:00
Yarmo Mackenbach
056ebd6d83
fix: fix IRC profile display value 2023-10-09 16:18:47 +02:00
Yarmo Mackenbach
34ad2d718d
fix: update lobste.rs URL syntax 2023-10-07 12:49:35 +02:00
Yarmo Mackenbach
a616dcd66d
fix: make IRC fetcher compatible with aspe 2023-10-07 12:39:22 +02:00
Yarmo Mackenbach
8cd40ea422
feat: add JSON schemas 2023-10-06 21:40:45 +02:00
Yarmo Mackenbach
8a01cdf9fa
chore: release 1.2.5 2023-10-05 11:14:38 +02:00
Yarmo Mackenbach
a3c9e9137d
feat: support themeColor 2023-10-05 11:11:25 +02:00
Yarmo Mackenbach
2dd18395dd
chore: release 1.2.4 2023-10-04 09:13:43 +02:00
Yarmo Mackenbach
fc0af3dd1d
feat: modify display object 2023-10-04 09:11:02 +02:00
Yarmo Mackenbach
34688bb644
chore: release 1.2.3 2023-10-03 13:47:26 +02:00
Yarmo Mackenbach
c5d6c2c9d9
fix: ambiguity logic 2023-10-03 13:45:05 +02:00
Yarmo Mackenbach
0d1e400d02
chore: release 1.2.2 2023-10-03 12:05:09 +02:00
Yarmo Mackenbach
7e5d15ac0f
fix: display data logic in claim toJSON 2023-09-25 16:42:16 +02:00
Yarmo Mackenbach
c1cb3fcbb6
fix: fix service provider information 2023-09-25 11:55:59 +02:00
Yarmo Mackenbach
a25e94002c
chore: release 1.2.1 2023-09-23 10:28:24 +02:00
Yarmo Mackenbach
bd864b796d
chore: release 1.2.0 2023-09-23 10:16:41 +02:00
Yarmo Mackenbach
ea8eb234ad
feat: support forgejo 2023-09-23 09:45:15 +02:00
Yarmo Mackenbach
264645b381
feat: add verification result validation function 2023-09-23 09:44:51 +02:00
Yarmo Mackenbach
ba6941448c
chore: release 1.1.1 2023-09-22 11:00:36 +02:00
Yarmo Mackenbach
beb78e8227
fix: normalize case before hash verification 2023-09-22 08:59:33 +02:00
Yarmo Mackenbach
9c9b387fc9
chore: release 1.1.0 2023-09-21 14:49:15 +02:00
Yarmo Mackenbach
cb397e42b7
feat: unify fromJSON() for Profile, Persona and Claim 2023-09-21 14:26:26 +02:00
Yarmo Mackenbach
4aba882b1b
chore: release 1.0.4 2023-09-19 15:35:55 +02:00
Yarmo Mackenbach
1baffe9c92
fix: allow the activitypub Person request to fail 2023-09-19 15:31:44 +02:00
Yarmo Mackenbach
feda782136
chore: release 1.0.3 2023-09-19 13:37:41 +02:00
Yarmo Mackenbach
81d5ba0d57
fix: avoid using potentially missing URL 2023-09-19 13:35:11 +02:00
Yarmo Mackenbach
cae1020f45
chore: release 1.0.2 2023-09-19 12:57:57 +02:00
Yarmo Mackenbach
c2f9efa698
fix: make nodeinfo requests use HTTPS 2023-09-19 12:54:37 +02:00
Yarmo Mackenbach
549a86c121
chore: release 1.0.1 2023-09-18 17:46:54 +02:00
Yarmo Mackenbach
d8529a7f92
feat: add prerelease checks script 2023-09-18 17:34:54 +02:00
Yarmo Mackenbach
2a314bef52
fix: fix import calls preventing proper bundling 2023-09-18 12:49:35 +02:00
Yarmo Mackenbach
8ea07938e4 feat: use nodeinfo for fediverse instance info 2023-09-18 08:18:17 +00:00
Yarmo Mackenbach
cb841fe9b7 feat: better support both Person and Note data 2023-09-18 08:18:17 +00:00
Yarmo Mackenbach
09b052c7b9
fix: ignore OpenPGP users without userId 2023-09-17 11:39:00 +02:00
Yarmo Mackenbach
0dfb0bc38a
fix: fix ci syntax 2023-09-14 18:20:09 +02:00
Preston Maness
027d7e1a4b Use more generic Account entity rather than Collective entity
As per

https://graphql-docs-v2.opencollective.com/types/Account

>Account interface shared by all kind of accounts (Bot, Collective,
Event, User, Organization)
2023-08-16 16:34:49 -05:00
Yarmo Mackenbach
75ec28b618
chore: update CHANGELOG 2023-07-13 10:53:43 +02:00
Yarmo Mackenbach
89ab8cccc1
chore: new version 1.0.0 2023-07-13 10:44:27 +02:00
Yarmo Mackenbach
b4ac82b010
chore: update builds 2023-07-13 10:41:31 +02:00
Yarmo Mackenbach
a8a97b2d85
feat: minor tweaks 2023-07-13 10:40:35 +02:00
Yarmo Mackenbach
c3f7df2113
fix: legacy signature functionality 2023-07-13 10:40:02 +02:00
Yarmo Mackenbach
bc5fe110a7
fix: include missing data when creating profiles 2023-07-13 10:39:13 +02:00
Yarmo Mackenbach
77bfb03ea6
feat: move to new API 2023-07-13 10:38:13 +02:00
Yarmo Mackenbach
0c0d9a9ec3
fix: fix obsolete calls 2023-07-13 10:37:36 +02:00
Yarmo Mackenbach
e6228ed22e
chore: new version 1.0.0-rc.0 2023-07-10 11:51:37 +02:00
Yarmo Mackenbach
65752d0dde
chore: update builds 2023-07-10 11:46:53 +02:00
Yarmo Mackenbach
128c9bf682
fix: fix compliance with spec by including fetcher 2023-07-10 10:40:06 +02:00
Yarmo Mackenbach
decda24d26
fix: fix linting issues 2023-07-10 10:39:00 +02:00
Yarmo Mackenbach
6eb2435127
fix: fix compliance with Keyoxide v2 spec 2023-07-10 10:25:17 +02:00
Yarmo Mackenbach
58561d6e0d
chore: update builds 2023-07-09 12:05:21 +02:00
Yarmo Mackenbach
15dca5a771
feat: update yarn.lock 2023-07-09 12:04:59 +02:00
Yarmo Mackenbach
0ba5de77e7
feat: temporarily remove rome 2023-07-09 12:04:38 +02:00
Yarmo Mackenbach
0546cc1e49
feat: tweaks to examples, use new classes 2023-07-09 12:03:51 +02:00
Yarmo Mackenbach
00d646e2b2
feat: tweaks to src, use new classes and enums 2023-07-09 12:03:25 +02:00
Yarmo Mackenbach
149ac6f71e
feat: refactor keys to openpgp, use Profile class 2023-07-09 12:01:09 +02:00
Yarmo Mackenbach
a30339272a
feat: update claim, persona, profile classes 2023-07-09 11:42:21 +02:00
Yarmo Mackenbach
fd8c760689
feat: apply ServiceProvider class, update tests 2023-07-09 11:31:25 +02:00
Yarmo Mackenbach
b674f113c7
feat: add ServiceProvider class 2023-07-09 11:28:50 +02:00
Yarmo Mackenbach
92d150efea
fix: remove obsolete dep 2023-07-08 08:46:02 +02:00
Yarmo Mackenbach
535d35bd48
feat: use rollup for bundling 2023-07-08 08:36:57 +02:00
Yarmo Mackenbach
0f7c444d3c
feat: convert CJS to ESM 2023-07-08 08:19:29 +02:00
Yarmo Mackenbach
7f1d972fa7
chore: release 0.19.0 2023-07-04 12:23:09 +02:00
Yarmo Mackenbach
f3f4c96f9b
fix: fix bundling 2023-07-04 12:19:11 +02:00
Yarmo Mackenbach
9f31bc5349
chore: update CHANGELOG 2023-07-03 16:58:02 +02:00
Yarmo Mackenbach
067c35a82c feat: support ASPE 2023-07-03 14:47:31 +00:00
Yarmo Mackenbach
73ae4b296f feat: add deps 2023-07-03 14:47:31 +00:00
Preston Maness
06f1cdbe51 Add tests 2023-07-03 10:23:07 +00:00
Preston Maness
e2d34723e5 Update tests, make sure old behaviour continues to work as expected 2023-07-03 10:23:07 +00:00
Preston Maness
be37a25352 Include updated test file for configurable schema 2023-07-03 10:23:07 +00:00
Preston Maness
0166a30e3c Make scheme of the proxy configurable
(Aids in local development where TLS certs are unavailable)
2023-07-03 10:23:07 +00:00
Preston Maness
f7c90edd7d Ensure ActivityPub claim displays are of for @user@domain.tld
Fixes #30
2023-07-03 10:16:29 +00:00
Preston Maness
bfe5a6f486 Add test for whitespace handling of plaintext proofs 2023-06-17 01:50:04 -05:00
Preston Maness
0e543946db When performing plaintext proof comparison, remove spaces before
searching for fingerprint

Fixes #22
2023-06-17 01:50:04 -05:00
Yarmo Mackenbach
542bab3232
fix: apply linting fixes 2023-06-16 15:38:47 +02:00
Yarmo Mackenbach
4f5e5592f4
fix: ignore non-issues without current solution 2023-05-03 15:35:40 +02:00
Yarmo Mackenbach
fceb5b6f9b
fix: fix usage of wrong types 2023-05-03 15:33:45 +02:00
Yarmo Mackenbach
53e73afa19
fix: fix and add missing JSDOC types 2023-05-03 15:32:54 +02:00
Yarmo Mackenbach
fb0aaa3e17
fix: fix imports 2023-05-03 15:27:01 +02:00
Yarmo Mackenbach
473916dc33
chore: update deps with fixed type definitions 2023-05-03 15:22:54 +02:00
Yarmo Mackenbach
3bcb724b7f
fix: fix calls to linting 2023-05-03 10:47:50 +02:00
Yarmo Mackenbach
0e29471d89
feat: add linting config 2023-05-03 10:46:09 +02:00
Yarmo Mackenbach
dc0806e738
feat: replace standard with eslint 2023-05-03 10:45:30 +02:00
Yarmo Mackenbach
46cffbf056
chore: Update CHANGELOG 2023-03-28 16:48:13 +02:00
Yarmo Mackenbach
1b1549bcc7
fix: Fix CI 2023-03-28 13:11:42 +02:00
Yarmo Mackenbach
d069569b32
fix: Fix selfCertifications order 2023-03-28 13:05:39 +02:00
Yarmo Mackenbach
28a3cb0e9a
feat: Move CI to woodpecker 2023-03-28 12:24:57 +02:00
Yarmo Mackenbach
231d93348d
chore: Release 0.18.3 2023-03-27 17:44:46 +02:00
Yarmo Mackenbach
a6ff593d94
chore: Update CHANGELOG 2023-03-27 11:09:25 +02:00
Yarmo Mackenbach
d6c31ef50c
fix: Fix forem ambiguity 2023-03-27 11:05:11 +02:00
Yarmo Mackenbach
15bbad0e6b
feat: Add OpenCollective service provider 2023-03-19 09:52:35 +01:00
Yarmo Mackenbach
7d65a21c1d
feat: Add GraphQL fetcher 2023-03-19 09:51:45 +01:00
Yarmo Mackenbach
166d8d5cf3 feat: Add EntityEncodingFormat to claim defs 2023-03-18 15:21:20 +01:00
Yarmo Mackenbach
650c389ae2 feat: Add EntityEncodingFormat enum 2023-03-18 15:21:20 +01:00
Yarmo Mackenbach
d943021579 feat: Add entities dep 2023-03-18 15:21:20 +01:00
Yarmo Mackenbach
955bbd8a08
feat: Add matrix match backwards compatible test 2023-03-16 11:25:34 +01:00
Yarmo Mackenbach
92ce86a6e0
feat: Add support for Keybase claims 2023-03-12 14:51:38 +01:00
Yarmo Mackenbach
322b2c4529
feat: Replace devto with forem 2023-03-12 14:23:24 +01:00
Yarmo Mackenbach
e85d77045c
fix: Remove dep 2023-03-12 14:00:45 +01:00
Yarmo Mackenbach
e98995ec0d fix: Apply rome linter fixes 2023-03-12 13:55:27 +01:00
Yarmo Mackenbach
e2a344848c Add rome dependency 2023-03-12 13:55:25 +01:00
Yarmo Mackenbach
976869aa9c
fix: Fix npm publish step in drone 2023-03-08 14:40:08 +01:00
Yarmo Mackenbach
0d9b91cf62
Release 0.18.2 2023-03-08 14:18:16 +01:00
Yarmo Mackenbach
4db27c4876
feat: Upgrade dependencies 2023-03-08 14:15:57 +01:00
Yarmo Mackenbach
025cd12aba
feat: Remove query-string dep 2023-03-08 14:15:06 +01:00
Yarmo Mackenbach
3d643afdfa
fix: Fix Matrix URI format
As per https://spec.matrix.org/latest/appendices/#matrix-uri-scheme, Matrix URIs should not contain any sigils.
2023-03-08 13:46:41 +01:00
Yarmo Mackenbach
e84f09be5d
fix: Fix timeout in test 2023-03-06 21:54:34 +01:00
Yarmo Mackenbach
525e876ad1
Merge branch 'dev' 2023-02-15 15:31:13 +01:00
Yarmo Mackenbach
a017433ca6
Merge branch 'tyman-main' 2023-02-15 15:29:09 +01:00
TymanWasTaken
06b7d24cce
Modify the twitter claim definition so that it requires no authorization, by making use of twitter's oembed support
The new twitter api changes will remove free access to the api, and doing verification without any credentials is easier anyways
2023-02-14 23:48:26 -07:00
Yarmo Mackenbach
f3ce2accd4
Temporarely disable forgejo
There currently is no way to distinguish a forgejo server from a gitea server.
2023-01-22 15:17:36 +01:00
Yarmo Mackenbach
f4d26cc15f
Update CHANGELOG 2023-01-18 14:02:16 +01:00
Yarmo Mackenbach
f5ea8fd549
Add forgejo support 2023-01-18 13:59:20 +01:00
Yarmo Mackenbach
eb72827887
Remove gitea repo name restriction 2023-01-18 13:56:37 +01:00
Yarmo Mackenbach
1ffc8b4175
Release 0.18.1 2022-12-12 16:28:21 +01:00
Yarmo Mackenbach
4385fa1e51
Support new XMPP proofs 2022-12-12 16:20:55 +01:00
Yarmo Mackenbach
05fa92063a
Improve XMPP parsing, remove jsdom dependency 2022-12-04 17:28:04 +01:00
Yarmo Mackenbach
1720c59093
Fix missing user-agent headers 2022-11-30 22:29:54 +01:00
Yarmo Mackenbach
bfc64ce898
Release 0.18.0 2022-11-17 22:07:16 +01:00
Yarmo Mackenbach
421f907206
Remove proxy functionality 2022-11-17 22:01:47 +01:00
Yarmo Mackenbach
f46b32528d
Update CHANGELOG 2022-11-17 21:25:06 +01:00
Yarmo Mackenbach
2efa2cbde8
Fix types in test 2022-11-17 21:11:00 +01:00
Yarmo Mackenbach
c7bd4fe81e
Remove null values 2022-11-17 21:09:42 +01:00
Yarmo Mackenbach
e9fdeb0bf8
Change enum values to strings 2022-11-17 20:52:34 +01:00
Yarmo Mackenbach
56b2722ce0
Update CHANGELOG 2022-11-17 20:24:12 +01:00
Yarmo Mackenbach
a3f14228a5 Merge pull request 'Support fediverse verification through posts' (#29) from support-fediverse-posts into main
Reviewed-on: https://codeberg.org/keyoxide/doipjs/pulls/29
2022-11-17 19:21:54 +00:00
Yarmo Mackenbach
82ee7e2dda Merge branch 'main' into support-fediverse-posts 2022-11-17 19:21:26 +00:00
Yarmo Mackenbach
1e37969dc6
Make data structure uniform 2022-11-16 22:25:32 +01:00
Yarmo Mackenbach
643dabff9f
Support fediverse verification through posts 2022-11-16 21:27:24 +01:00
Yarmo Mackenbach
4ff11947e4
Remove obsolete service providers 2022-11-16 20:26:46 +01:00
Yarmo Mackenbach
66e3e68e2f
Release 0.17.5 2022-11-14 23:54:51 +01:00
Yarmo Mackenbach
13e20fe6fb
Fix implementation of postprocess function 2022-11-14 23:49:40 +01:00
Yarmo Mackenbach
e6b824bcf8
Release 0.17.4 2022-11-14 19:17:21 +01:00
Yarmo Mackenbach
ca6aec04dd
Add fetcher to exports 2022-11-14 19:16:04 +01:00
Yarmo Mackenbach
a0c1bdaa90
Release 0.17.2 2022-10-27 22:17:03 +02:00
Yarmo Mackenbach
fcc62a6025
Fix proxy verification options 2022-10-27 21:47:41 +02:00
Yarmo Mackenbach
6d89cc0542
Release 0.17.1 2022-10-25 10:26:24 +02:00
Yarmo Mackenbach
2c722d31a9
Fix verification logic 2022-10-25 09:22:25 +02:00
Yarmo Mackenbach
f64f9cff58
Fix ambiguous parameter 2022-10-25 09:22:06 +02:00
Yarmo Mackenbach
cabaa4ec16
Release 0.17.0 2022-10-24 21:59:11 +02:00
Yarmo Mackenbach
2d6b02eded Merge pull request 'Support ActivityPub identity verification' (#27) from support-activitypub into main
Reviewed-on: https://codeberg.org/keyoxide/doipjs/pulls/27
2022-10-24 21:44:58 +02:00
Yarmo Mackenbach
a46311f1f8
Fix header 2022-10-24 21:43:48 +02:00
Yarmo Mackenbach
516896c631
Fix header 2022-10-24 21:33:12 +02:00
Yarmo Mackenbach
38242bd1e6 Merge branch 'main' into support-activitypub 2022-10-14 13:06:00 +02:00
Yarmo Mackenbach
37fe998fd8
Do not react to lack of setup 2022-10-14 13:04:42 +02:00
Yarmo Mackenbach
3c1302c150
Fix minor bug, allow not setting up 2022-10-14 13:02:49 +02:00
Yarmo Mackenbach
6a7bc8b5b2
Release 0.16.4 2022-10-07 10:30:35 +02:00
Yarmo Mackenbach
802f8011dc
Add superuser URL detection 2022-10-07 10:20:02 +02:00
Yarmo Mackenbach
e0502c78bb
Fix rare null error 2022-10-07 10:18:52 +02:00
Yarmo Mackenbach
2a29832110
Remove webfinger from activitypub 2022-10-03 22:32:46 +02:00
Yarmo Mackenbach
1710195211
Update default options 2022-10-01 09:57:58 +02:00
Yarmo Mackenbach
007282ef22
Add AP proxy endpoint 2022-10-01 09:53:36 +02:00
Yarmo Mackenbach
c992b4fe28 Merge branch 'main' into support-activitypub 2022-10-01 00:10:33 +02:00
Yarmo Mackenbach
8b83dbd8ed
Release 0.16.3 2022-09-30 23:45:13 +02:00
Yarmo Mackenbach
220a70be61
Update openpgpjs dep 2022-09-30 23:40:52 +02:00
Yarmo Mackenbach
e28315e87f
Add activitypub service provider 2022-09-30 23:39:10 +02:00
Yarmo Mackenbach
8a1f8ad586
Allows multiple claim methods 2022-09-30 23:37:55 +02:00
121 changed files with 101277 additions and 25667 deletions

View file

@ -1,101 +0,0 @@
---
kind: pipeline
name: test
steps:
- name: run tests
image: node
commands:
- yarn
- yarn run prepare
- yarn run test
trigger:
event:
- push
- pull_request
- tag
---
kind: pipeline
name: publish-npm
steps:
- name: prepare
image: node
commands:
- yarn
- yarn run prepare
- name: publish on npm
image: plugins/npm
settings:
username: yarmo_eu
password:
from_secret: npm_token
email:
from_secret: npm_email
depends_on:
- test
trigger:
event:
- tag
---
kind: pipeline
name: publish-docker-latest
steps:
- name: publish latest proxy container
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
dockerfile: docker/proxy/Dockerfile
repo: keyoxide/doip-proxy
tags: latest
- name: build tag proxy container
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
dockerfile: docker/proxy/Dockerfile
repo: keyoxide/doip-proxy
auto_tag: true
depends_on:
- test
trigger:
event:
- tag
---
kind: pipeline
name: publish-docker-dev
steps:
- name: build dev proxy container
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
dockerfile: docker/proxy/Dockerfile
repo: keyoxide/doip-proxy
tags: dev
depends_on:
- test
trigger:
branch:
- main
event:
- push

22
.eslintrc.json Normal file
View file

@ -0,0 +1,22 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"standard",
"plugin:jsdoc/recommended"
],
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
},
"plugins": [
"jsdoc"
]
}

View file

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

View file

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

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

@ -1,5 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn run license:check
yarn test

View file

@ -12,11 +12,11 @@ ignore
docs
examples
\.husky
\.woodpecker
package.json
yarn.lock
rollup.config.js
\.editorconfig
\.gitignore
\.licenseignore
\.drone.yml
Dockerfile
\.licenseignore

View file

@ -0,0 +1,24 @@
when:
branch: main
event: tag
steps:
prepare:
image: node
commands:
- yarn --pure-lockfile
- yarn run prepare
publish-npm:
when:
branch: main
event: tag
image: plugins/npm
settings:
username: yarmo_eu
token:
from_secret: npm_token
email:
from_secret: npm_email
depends_on:
- test

7
.woodpecker/.test.yml Normal file
View file

@ -0,0 +1,7 @@
steps:
test:
image: node
commands:
- yarn --pure-lockfile
- yarn run prepare
- yarn run test

BIN
.yarn/install-state.gz Normal file

Binary file not shown.

6
.yarnrc.yml Normal file
View file

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

View file

@ -6,6 +6,168 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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
- OpenCollective claim verification
- Keybase claim verification
- GraphQL fetcher protocol
- HTML entity decoding in proofs
### Changed
- Replace devto with forem
### Fixed
- forem service provider ambiguity
## [0.18.2] - 2023-03-08
### Changed
- Use oembed for Twitter verification
### Removed
- query-string dependency
### Fixed
- Matrix URI format
## [0.18.1] - 2022-12-12
### Changed
- Improved XMPP proof requests
### Fixed
- Added missing user-agent headers
### Removed
- jsdom dependency
## [0.18.0] - 2022-11-17
### Changed
- Allow ActivityPub verification through posts
- Improve type consistency
### Removed
- Proxy server code
## [0.17.5] - 2022-11-14
### Fixed
- Implementation of postprocess function
## [0.17.3] - 2022-11-14
### Changed
- Add fetcher to lib exports
## [0.17.2] - 2022-10-27
### Fixed
- Proxy verification options
## [0.17.1] - 2022-10-25
### Fixed
- ActivityPub definition
- Claim verification logic
## [0.17.0] - 2022-10-24
### Added
- ActivityPub claim verification
## [0.16.4] - 2022-10-07
### Fixed
- superuser.com not being detected
## [0.16.3] - 2022-09-30
### Changed
- Updated dependencies
## [0.16.2] - 2022-09-21
### Added
- Support for hashed proofs

View file

@ -1,22 +1,18 @@
# 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 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).
[doip.js](https://codeberg.org/keyoxide/doipjs) allows websites and Node.js projects to verify decentralized online
identities.
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**:
@ -56,32 +52,32 @@ const verifyIdentity = async (url, fp) => {
verifyIdentity('dns:doip.rocks', '9f0048ac0b23301e1f77e994909f6bd6f80f485d')
```
This snippet works and will verify the [doip.rocks](https://doip.rocks) domain as
This snippet verifies the [doip.rocks](https://doip.rocks) domain as
bidirectionally linked to Yarmo's cryptographic key.
## About Keyoxide
## Contributing
[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.
Anyone can contribute!
## Community
Developers are invited to:
There's a [Keyoxide Matrix room](https://matrix.to/#/#keyoxide:matrix.org) where
we discuss everything DOIP and Keyoxide.
- fork the repository and play around
- submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/doipjs/issues)
## Donate
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.
Please consider [donating](https://liberapay.com/Keyoxide/) if you think this
project is a step in the right direction for the internet.
Everyone is invited to:
## Funding
- 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
This library was realized with funding from
[NLnet](https://nlnet.nl/project/Keyoxide/).
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.

14597
dist/doip.core.js vendored Normal file

File diff suppressed because one or more lines are too long

6
dist/doip.core.min.js vendored Normal file

File diff suppressed because one or more lines are too long

36961
dist/doip.fetchers.js vendored Normal file

File diff suppressed because one or more lines are too long

21
dist/doip.fetchers.min.js vendored Normal file

File diff suppressed because one or more lines are too long

36958
dist/doip.fetchers.minimal.js vendored Normal file

File diff suppressed because one or more lines are too long

21
dist/doip.fetchers.minimal.min.js vendored Normal file

File diff suppressed because one or more lines are too long

16408
dist/doip.js vendored

File diff suppressed because one or more lines are too long

21
dist/doip.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,6 +0,0 @@
FROM node:16-alpine
WORKDIR /app
COPY . .
RUN yarn --production --pure-lockfile
EXPOSE 3000
CMD yarn run proxy

View file

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

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

View file

@ -0,0 +1,20 @@
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,4 +1,4 @@
const doip = require('../src')
import * as doip from '../src/index.js'
const main = async () => {
// Obtain the plaintext public key
@ -28,14 +28,11 @@ fCRSXrr7SZxIu7I8jfQrxc0k9XhpPI/gdlgRqoEG2lMyqFaWzyoI9dyoVwji78rg
=Csr+
-----END PGP PUBLIC KEY BLOCK-----`
// 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)
// Use the plaintext key to get a profile
const profile = await doip.openpgp.fetchPlaintext(pubKeyPlaintext)
// Log the claims of the first UID
console.log(obj.users[0].claims)
console.log(profile.personas[0].claims)
}
main()

View file

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

View file

@ -0,0 +1,9 @@
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 @@
const doip = require('../src')
import * as doip from '../src/index.js'
const main = async () => {
// Generate the claim

10
jsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "ES2020",
"target": "ES2020",
"checkJs": true,
"moduleResolution": "node"
},
"include": ["src", "examples"],
"exclude": ["node_modules"]
}

View file

@ -1,5 +1,8 @@
{
"plugins": ["plugins/markdown"],
"plugins": [
"plugins/markdown",
"node_modules/jsdoc-tsimport-plugin"
],
"source": {
"include": ["./src", "./README.md"]
},
@ -12,21 +15,26 @@
}
},
"opts": {
"template": "node_modules/clean-jsdoc-theme",
"theme_opts": {
"theme": "light",
"menu": [
{
"title": "Source code",
"link": "https://codeberg.org/keyoxide/doipjs",
"target": "_blank"
},
{
"title": "Keyoxide",
"link": "https://keyoxide.org",
"target": "_blank"
}
]
"template": "node_modules/docdash",
"destination": "docs/"
},
"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"
}
}
}
}

View file

@ -1,63 +1,79 @@
{
"name": "doipjs",
"version": "0.16.2",
"description": "Decentralized OpenPGP Identity Proofs library in Node.js",
"name": "@myriation/doipjs",
"version": "1.2.9+myriaiton.1",
"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",
"dependencies": {
"@openpgp/hkp-client": "^0.0.2",
"@openpgp/wkd-client": "^0.0.3",
"@openpgp/hkp-client": "^0.0.3",
"@openpgp/wkd-client": "^0.0.4",
"@xmpp/client": "^0.13.1",
"@xmpp/debug": "^0.13.0",
"axios": "^0.25.0",
"axios": "^1.6.5",
"browser-or-node": "^1.3.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-validator": "^6.10.0",
"entities": "^4.4.0",
"hash-wasm": "^4.9.0",
"irc-upd": "^0.11.0",
"jsdom": "^20.0.0",
"jose": "^4.14.4",
"merge-options": "^3.0.3",
"openpgp": "^5.0",
"query-string": "^6.14.1",
"openpgp": "^5.5.0",
"rfc4648": "^1.5.2",
"valid-url": "^1.0.9",
"validator": "^13.5.2"
"validator": "^13.9.0"
},
"devDependencies": {
"browserify": "^17.0.0",
"browserify-shim": "^3.8.14",
"@rollup/plugin-commonjs": "^25.0.2",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"chai-match-pattern": "^1.2.0",
"clean-jsdoc-theme": "^3.2.4",
"docdash": "^2.0.2",
"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": "^3.6.6",
"jsdoc": "^4.0.2",
"jsdoc-tsimport-plugin": "^1.0.5",
"license-check-and-add": "^4.0.3",
"lint-staged": "^11.0.0",
"minify": "^9.1",
"mocha": "^9.2.0",
"nodemon": "^2.0.19",
"standard": "^16.0.3"
"rollup": "^3.26.2",
"rollup-plugin-polyfill-node": "^0.12.0",
"rollup-plugin-visualizer": "^5.9.2"
},
"scripts": {
"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 jsdom -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",
"standard:check": "./node_modules/.bin/standard ./src",
"standard:fix": "./node_modules/.bin/standard --fix ./src",
"mocha": "./node_modules/.bin/mocha",
"test": "yarn run standard:check && yarn run license:check && yarn run mocha",
"proxy": "NODE_ENV=production node ./src/proxy/",
"proxy:dev": "NODE_ENV=development ./node_modules/.bin/nodemon ./src/proxy/",
"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",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"test": "yarn lint && yarn run license:check && yarn run mocha",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "https://codeberg.org/keyoxide/doipjs"
"url": "https://git.myriation.org/myriation/doipjs"
},
"homepage": "https://js.doip.rocks",
"keywords": [
@ -69,13 +85,5 @@
"identity"
],
"author": "Yarmo Mackenbach <yarmo@yarmo.eu> (https://yarmo.eu)",
"license": "Apache-2.0",
"browserify": {
"transform": [
"browserify-shim"
]
},
"browserify-shim": {
"openpgp": "global:openpgp"
}
"license": "Apache-2.0"
}

33
prerelease.js Normal file
View file

@ -0,0 +1,33 @@
/*
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()

73
rollup.config.js Normal file
View file

@ -0,0 +1,73 @@
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()
]
}
]

183
src/asp.js Normal file
View file

@ -0,0 +1,183 @@
/*
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 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 SupportedCryptoAlg = ['EdDSA', 'ES256', 'ES256K', 'ES384', 'ES512']
/**
* Functions related to Ariadne Signature Profiles
* @module asp
*/
/**
* Fetch a public key using Web Key Directory
* @function
* @param {string} uri - ASPE URI
* @returns {Promise<Profile>} The fetched profile
* @example
* const key = await doip.aspe.fetchASPE('aspe:domain.example:1234567890');
*/
export async function fetchASPE (uri) {
const re = /aspe:(.*):(.*)/
if (!re.test(uri)) {
throw new Error('Invalid ASPE URI')
}
const matches = uri.match(re)
const domainPart = matches[1]
const localPart = matches[2].toUpperCase()
const profileUrl = `https://${domainPart}/.well-known/aspe/id/${localPart}`
let profileJws
try {
profileJws = await axios.get(
profileUrl,
{
responseType: 'text'
}
)
.then((/** @type {import('axios').AxiosResponse} */ response) => {
if (response.status === 200) {
return response
}
})
.then((/** @type {import('axios').AxiosResponse} */ response) => response.data)
} catch (e) {
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
}
/**
* Parse a JWS and extract the profile it contains
* @function
* @param {string} profileJws - Compact-Serialized profile JWS
* @param {string} uri - The ASPE URI associated with the profile
* @returns {Promise<Profile>} The extracted profile
* @example
* const key = await doip.aspe.parseProfileJws('...', 'aspe:domain.example:123');
*/
export async function parseProfileJws (profileJws, uri) {
const matches = uri.match(/aspe:(.*):(.*)/)
const localPart = matches[2].toUpperCase()
// Decode the headers
const protectedHeader = decodeProtectedHeader(profileJws)
// Extract the JWK
if (!SupportedCryptoAlg.includes(protectedHeader.alg)) {
throw new Error('Invalid profile JWS: wrong key algorithm')
}
if (!protectedHeader.kid) {
throw new Error('Invalid profile JWS: missing key identifier')
}
if (!protectedHeader.jwk) {
throw new Error('Invalid profile JWS: missing key')
}
const publicKey = await importJWK(protectedHeader.jwk, protectedHeader.alg)
// Compute and verify the fingerprint
const fp = await computeJwkFingerprint(protectedHeader.jwk)
if (fp !== protectedHeader.kid) {
throw new Error('Invalid profile JWS: wrong key')
}
if (localPart && fp !== localPart) {
throw new Error('Invalid profile JWS: wrong key')
}
// Decode the payload
const { payload } = await compactVerify(profileJws, publicKey)
const payloadJson = JSON.parse(new TextDecoder().decode(payload))
// Verify the payload
if (!(Object.prototype.hasOwnProperty.call(payloadJson, 'http://ariadne.id/type') && payloadJson['http://ariadne.id/type'] === 'profile')) {
throw new Error('Invalid profile JWS: JWS is not a profile')
}
if (!(Object.prototype.hasOwnProperty.call(payloadJson, 'http://ariadne.id/version') && payloadJson['http://ariadne.id/version'] === 0)) {
throw new Error('Invalid profile JWS: profile version not supported')
}
// Extract data from the payload
/** @type {string} */
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>} */
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 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
}
/**
* Compute the fingerprint for {@link https://github.com/panva/jose/blob/main/docs/interfaces/types.JWK.md JWK} keys
* @function
* @param {import('jose').JWK} key - The JWK public key for which to compute the fingerprint
* @returns {Promise<string>} The computed fingerprint
*/
export async function computeJwkFingerprint (key) {
const thumbprint = await calculateJwkThumbprint(key, 'sha512')
const fingerprintBytes = base64url.parse(thumbprint, { loose: true }).slice(0, 16)
const fingerprint = base32.stringify(fingerprintBytes, { pad: false })
return fingerprint
}

View file

@ -13,74 +13,105 @@ 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 validator = require('validator')
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')
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'
/**
* @class
* @classdesc OpenPGP-based identity claim
* @classdesc Identity claim
* @property {string} uri - The claim's URI
* @property {string} fingerprint - The fingerprint to verify the claim against
* @property {string} status - The current status of the claim
* @property {number} status - The current status code of the claim
* @property {Array<object>} matches - The claim definitions matched against the URI
* @property {object} verification - The result of the verification process
* @example
* const claim = doip.Claim();
* const claim = doip.Claim('dns:domain.tld?type=TXT');
* const claim = doip.Claim('dns:domain.tld?type=TXT', '123abc123abc');
*/
class Claim {
export class Claim {
/**
* Initialize a Claim object
* @constructor
* @param {string|object} [uri] - The URI of the identity claim or a JSONified Claim instance
* @param {string} [uri] - The URI of the identity claim
* @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');
* 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 && !validUrl.isUri(uri)) {
if (uri && !isUri(uri)) {
throw new Error('Invalid URI')
}
// Verify validity of fingerprint
if (fingerprint) {
try {
validator.isAlphanumeric(fingerprint)
// @ts-ignore
isAlphanumeric.default(fingerprint)
} catch (err) {
throw new Error('Invalid fingerprint')
}
}
this._uri = uri || null
this._fingerprint = fingerprint || null
this._status = E.ClaimStatus.INIT
this._matches = null
this._verification = null
/**
* @type {string}
*/
this._uri = uri || ''
/**
* @type {string}
*/
this._fingerprint = fingerprint || ''
/**
* @type {number}
*/
this._status = ClaimStatus.INIT
/**
* @type {Array<ServiceProvider>}
*/
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
}
get uri () {
@ -96,27 +127,20 @@ class Claim {
}
get matches () {
if (this._status === E.ClaimStatus.INIT) {
if (this._status === 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 !== E.ClaimStatus.INIT) {
if (this._status !== ClaimStatus.INIT) {
throw new Error(
'Cannot change the URI, this claim has already been matched'
)
}
// Verify validity of URI
if (uri && !validUrl.isUri(uri)) {
if (uri.length > 0 && !isUri(uri)) {
throw new Error('The URI was invalid')
}
// Remove leading and trailing spaces
@ -126,7 +150,7 @@ class Claim {
}
set fingerprint (fingerprint) {
if (this._status === E.ClaimStatus.VERIFIED) {
if (this._status === ClaimStatus.VERIFIED) {
throw new Error(
'Cannot change the fingerprint, this claim has already been verified'
)
@ -142,26 +166,22 @@ 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 !== E.ClaimStatus.INIT) {
if (this._status !== ClaimStatus.INIT) {
throw new Error('This claim was already matched')
}
if (this._uri === null) {
if (this._uri.length === 0 || !isUri(this._uri)) {
throw new Error('This claim has no URI')
}
this._matches = []
claimDefinitions.list.every((name, i) => {
const def = claimDefinitions.data[name]
list.every((name, i) => {
const def = _data[name]
// If the candidate is invalid, continue matching
if (!def.reURI.test(this._uri)) {
@ -169,7 +189,12 @@ class Claim {
}
const candidate = def.processURI(this._uri)
if (candidate.match.isAmbiguous) {
// If the candidate could not be processed, continue matching
if (!candidate) {
return true
}
if (candidate.claim.uriIsAmbiguous) {
// Add to the possible candidates
this._matches.push(candidate)
} else {
@ -182,7 +207,7 @@ class Claim {
return true
})
this._status = E.ClaimStatus.MATCHED
this._status = this._matches.length === 0 ? ClaimStatus.NO_MATCHES : ClaimStatus.MATCHED
}
/**
@ -190,51 +215,49 @@ 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 {object} [opts] - Options for proxy, fetchers
* @param {import('./types').VerificationConfig} [opts] - Options for proxy, fetchers
*/
async verify (opts) {
if (this._status === E.ClaimStatus.INIT) {
if (this._status === ClaimStatus.INIT) {
throw new Error('This claim has not yet been matched')
}
if (this._status === E.ClaimStatus.VERIFIED) {
if (this._status >= 200) {
throw new Error('This claim has already been verified')
}
if (this._fingerprint === null) {
if (this._fingerprint.length === 0) {
throw new Error('This claim has no fingerprint')
}
// Handle options
opts = mergeOptions(defaults.opts, opts || {})
opts = mergeOptions(_opts, opts || {})
// If there are no matches
if (this._matches.length === 0) {
this._verification = {
result: false,
completed: true,
proof: {},
errors: ['No matches for claim']
}
this.status = ClaimStatus.NO_MATCHES
}
// For each match
for (let index = 0; index < this._matches.length; index++) {
const claimData = this._matches[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 proofs.fetch(claimData, opts)
proofData = await fetch(claimData, opts)
} catch (err) {
proofFetchError = err
}
if (proofData) {
// Run the verification process
verificationResult = await verifications.run(
verificationResult = await run(
proofData.result,
claimData,
this._fingerprint
@ -243,40 +266,43 @@ class Claim {
fetcher: proofData.fetcher,
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
if (def.functions?.postprocess) {
try {
({ claimData, proofData } = await def.functions.postprocess(claimData, proofData, opts))
} catch (_) {}
}
} else {
// Consider the proof completed but with a negative result
verificationResult = verificationResult || {
result: false,
completed: true,
proof: {},
proof: null,
errors: [proofFetchError]
}
if (this.isAmbiguous()) {
// Assume a wrong match and continue
continue
}
}
if (verificationResult.completed) {
// Store the result, keep a single match and stop verifying
this._verification = verificationResult
if (this.isAmbiguous() && !verificationResult.result) {
// Assume a wrong match and continue
continue
}
if (verificationResult.result) {
this._status = verificationResult.proof.viaProxy ? ClaimStatus.VERIFIED_VIA_PROXY : ClaimStatus.VERIFIED
this._matches = [claimData]
index = this._matches.length
}
}
// Fail safe verification result
this._verification = this._verification
? this._verification
: {
result: false,
completed: true,
proof: {},
errors: ['Unknown error']
}
this._status = E.ClaimStatus.VERIFIED
this._status = this._status >= 200 ? this._status : ClaimStatus.NO_PROOF_FOUND
}
/**
@ -284,34 +310,115 @@ 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}
* @returns {boolean} Whether the claim is ambiguous
*/
isAmbiguous () {
if (this._status === E.ClaimStatus.INIT) {
if (this._status < ClaimStatus.MATCHED) {
throw new Error('The claim has not been matched yet')
}
if (this._matches.length === 0) {
throw new Error('The claim has no matches')
}
return this._matches.length > 1 || this._matches[0].match.isAmbiguous
if (this._status >= 200 && this._status < 300) return false
return this._matches.length > 1 || this._matches[0].claim.uriIsAmbiguous
}
/**
* Get a JSON representation of the Claim object. Useful when transferring
* data between instances/machines.
* @function
* @returns {object}
* @returns {object} JSON reprentation of the claim
*/
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: 1,
claimVersion: 2,
uri: this._uri,
fingerprint: this._fingerprint,
proofs: [this._fingerprint],
matches: this._matches.map(x => x.toJSON()),
status: this._status,
matches: this._matches,
verification: this._verification
display: {
profileName: displayProfileName,
profileUrl: displayProfileUrl,
proofUrl: displayProofUrl,
serviceProviderName: displayServiceProviderName,
serviceProviderId: displayServiceProviderId
}
}
}
}
module.exports = Claim
/**
* @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
}

View file

@ -1,74 +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.
*/
const E = require('../enums')
const reURI = /^https:\/\/dev\.to\/(.*)\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'devto'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: match[1],
uri: `https://dev.to/${match[1]}`,
qr: null
},
proof: {
uri: uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://dev.to/api/articles/${match[1]}/${match[2]}`,
format: E.ProofFormat.JSON
}
}
},
claim: {
format: E.ClaimFormat.URI,
relation: E.ClaimRelation.CONTAINS,
path: ['body_markdown']
}
}
}
const tests = [
{
uri: 'https://dev.to/alice/post',
shouldMatch: true
},
{
uri: 'https://dev.to/alice/post/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/post',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -1,74 +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.
*/
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: uri,
qr: null
},
proof: {
uri: 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,
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

@ -1,73 +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.
*/
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,
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

@ -1,74 +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.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/(.*)\/gitea_proof\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
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
},
proof: {
uri: uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.NOCORS,
format: E.ProofFormat.JSON,
data: {
url: `https://${match[1]}/api/v1/repos/${match[2]}/gitea_proof`,
format: E.ProofFormat.JSON
}
}
},
claim: {
format: E.ClaimFormat.URI,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}
}
}
const tests = [
{
uri: 'https://domain.org/alice/gitea_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/gitea_proof/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/other_proof',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -1,74 +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.
*/
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: 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,
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

@ -1,74 +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.
*/
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: 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,
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

@ -1,63 +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.
*/
const list = [
'dns',
'irc',
'xmpp',
'matrix',
'telegram',
'twitter',
'reddit',
'liberapay',
'lichess',
'hackernews',
'lobsters',
'devto',
'gitea',
'gitlab',
'github',
'mastodon',
'pleroma',
'discourse',
'owncast',
'stackexchange'
]
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'),
devto: require('./devto'),
gitea: require('./gitea'),
gitlab: require('./gitlab'),
github: require('./github'),
mastodon: require('./mastodon'),
pleroma: require('./pleroma'),
discourse: require('./discourse'),
owncast: require('./owncast'),
stackexchange: require('./stackexchange')
}
exports.list = list
exports.data = data

View file

@ -1,78 +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.
*/
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: 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,
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

@ -1,74 +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.
*/
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: uri,
qr: null
},
proof: {
uri: 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,
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

@ -1,74 +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.
*/
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: 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,
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

@ -1,74 +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.
*/
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: 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,
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

@ -1,74 +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.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/@(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'mastodon'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `@${match[2]}@${match[1]}`,
uri: uri,
qr: null
},
proof: {
uri: uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: uri,
format: E.ProofFormat.JSON
}
}
},
claim: {
format: E.ClaimFormat.FINGERPRINT,
relation: E.ClaimRelation.CONTAINS,
path: ['attachment', 'value']
}
}
}
const tests = [
{
uri: 'https://domain.org/@alice',
shouldMatch: true
},
{
uri: 'https://domain.org/@alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -1,93 +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.
*/
const E = require('../enums')
const queryString = require('query-string')
const reURI = /^matrix:u\/(?:@)?([^@:]*:[^?]*)(\?.*)?/
const processURI = (uri) => {
const match = uri.match(reURI)
if (!match[2]) {
return null
}
const params = queryString.parse(match[2])
if (!('org.keyoxide.e' in params && 'org.keyoxide.r' in params)) {
return null
}
const profileUrl = `https://matrix.to/#/@${match[1]}`
const eventUrl = `https://matrix.to/#/${params['org.keyoxide.r']}/${params['org.keyoxide.e']}`
return {
serviceprovider: {
type: 'communication',
name: 'matrix'
},
match: {
regularExpression: reURI,
isAmbiguous: false
},
profile: {
display: `@${match[1]}`,
uri: profileUrl,
qr: null
},
proof: {
uri: eventUrl,
request: {
fetcher: E.Fetcher.MATRIX,
access: E.ProofAccess.GRANTED,
format: E.ProofFormat.JSON,
data: {
eventId: params['org.keyoxide.e'],
roomId: params['org.keyoxide.r']
}
}
},
claim: {
format: E.ClaimFormat.URI,
relation: E.ClaimRelation.CONTAINS,
path: ['content', 'body']
}
}
}
const tests = [
{
uri:
'matrix:u/alice:matrix.domain.org?org.keyoxide.r=!123:domain.org&org.keyoxide.e=$123',
shouldMatch: true
},
{
uri: 'matrix:u/alice:matrix.domain.org',
shouldMatch: true
},
{
uri: 'xmpp:alice@domain.org',
shouldMatch: false
},
{
uri: 'https://domain.org/@alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -1,78 +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.
*/
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: 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,
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

@ -1,74 +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.
*/
const E = require('../enums')
const reURI = /^https:\/\/(.*)\/users\/(.*)\/?/
const processURI = (uri) => {
const match = uri.match(reURI)
return {
serviceprovider: {
type: 'web',
name: 'pleroma'
},
match: {
regularExpression: reURI,
isAmbiguous: true
},
profile: {
display: `@${match[2]}@${match[1]}`,
uri: uri,
qr: null
},
proof: {
uri: uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
data: {
url: uri,
format: E.ProofFormat.JSON
}
}
},
claim: {
format: E.ClaimFormat.FINGERPRINT,
relation: E.ClaimRelation.CONTAINS,
path: ['summary']
}
}
}
const tests = [
{
uri: 'https://domain.org/users/alice',
shouldMatch: true
},
{
uri: 'https://domain.org/users/alice/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

@ -1,73 +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.
*/
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: uri,
request: {
fetcher: E.Fetcher.TWITTER,
access: E.ProofAccess.GRANTED,
format: E.ProofFormat.TEXT,
data: {
tweetId: match[2]
}
}
},
claim: {
format: E.ClaimFormat.URI,
relation: E.ClaimRelation.CONTAINS,
path: []
}
}
}
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

@ -1,74 +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.
*/
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: uri,
qr: uri
},
proof: {
uri: null,
request: {
fetcher: E.Fetcher.XMPP,
access: E.ProofAccess.SERVER,
format: E.ProofFormat.TEXT,
data: {
id: `${match[1]}@${match[2]}`,
field: 'note'
}
}
},
claim: {
format: E.ClaimFormat.URI,
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

25
src/constants.js Normal file
View file

@ -0,0 +1,25 @@
/*
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.
*/
const E = require('./enums')
import { ProxyPolicy } from './enums.js'
/**
* Contains default values
@ -21,46 +21,34 @@ const E = require('./enums')
*/
/**
* 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.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.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
* @property {object} claims.twitter - Options related to the verification of Twitter claims
* @property {string|null} claims.twitter.bearerToken - The Twitter API's bearer token ({@link https://developer.twitter.com/en/docs/authentication/oauth-2-0/bearer-tokens|Twitter docs})
* The default claim verification config used throughout the library
* @type {import('./types').VerificationConfig}
*/
const opts = {
export const opts = {
proxy: {
hostname: null,
policy: E.ProxyPolicy.NEVER
policy: ProxyPolicy.NEVER
},
claims: {
activitypub: {
url: null,
privateKey: null
},
irc: {
nick: null
nick: null,
sasl: []
},
matrix: {
instance: null,
accessToken: null
},
telegram: {
token: null
},
xmpp: {
service: null,
username: null,
password: null
},
twitter: {
bearerToken: null
}
}
}
exports.opts = opts

View file

@ -23,7 +23,7 @@ limitations under the License.
* @readonly
* @enum {string}
*/
const ProxyPolicy = {
export const ProxyPolicy = {
/** Proxy usage decision depends on environment and service provider */
ADAPTIVE: 'adaptive',
/** Always use a proxy */
@ -31,108 +31,183 @@ 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}
*/
const Fetcher = {
/** Basic HTTP requests */
HTTP: 'http',
export const Fetcher = {
/** HTTP requests to ActivityPub */
ACTIVITYPUB: 'activitypub',
/** ASPE HTTP requests */
ASPE: 'aspe',
/** DNS module from Node.js */
DNS: 'dns',
/** GraphQL over HTTP requests */
GRAPHQL: 'graphql',
/** Basic HTTP requests */
HTTP: 'http',
/** IRC module from Node.js */
IRC: 'irc',
/** XMPP module from Node.js */
XMPP: 'xmpp',
/** HTTP request to Matrix API */
MATRIX: 'matrix',
/** HKP and WKS request for OpenPGP */
OPENPGP: 'openpgp',
/** HTTP request to Telegram API */
TELEGRAM: 'telegram',
/** HTTP request to Twitter API */
TWITTER: 'twitter'
/** XMPP module from Node.js */
XMPP: 'xmpp'
}
/**
* Entity encoding format
* @readonly
* @enum {string}
*/
export const EntityEncodingFormat = {
/** No special formatting */
PLAIN: 'plain',
/** HTML encoded entities */
HTML: 'html',
/** XML encoded entities */
XML: 'xml'
}
Object.freeze(Fetcher)
/**
* Levels of access restriction for proof fetching
* @readonly
* @enum {number}
* @enum {string}
*/
const ProofAccess = {
export const ProofAccessRestriction = {
/** Any HTTP request will work */
GENERIC: 0,
NONE: 'none',
/** CORS requests are denied */
NOCORS: 1,
NOCORS: 'nocors',
/** HTTP requests must contain API or access tokens */
GRANTED: 2,
GRANTED: 'granted',
/** Not accessible by HTTP request, needs server software */
SERVER: 3
SERVER: 'server'
}
Object.freeze(ProofAccess)
/**
* Format of proof
* @readonly
* @enum {string}
*/
const ProofFormat = {
export const ProofFormat = {
/** JSON format */
JSON: 'json',
/** Plaintext format */
TEXT: 'text'
}
Object.freeze(ProofFormat)
/**
* Format of claim
* @readonly
* @enum {number}
* @enum {string}
*/
const ClaimFormat = {
export const ClaimFormat = {
/** `openpgp4fpr:123123123` */
URI: 0,
URI: 'uri',
/** `123123123` */
FINGERPRINT: 1
FINGERPRINT: 'fingerprint'
}
Object.freeze(ClaimFormat)
/**
* How to find the claim inside the proof's JSON data
* How to find the proof inside the fetched data
* @readonly
* @enum {number}
* @enum {string}
*/
const ClaimRelation = {
export const ClaimRelation = {
/** Claim is somewhere in the JSON field's textual content */
CONTAINS: 0,
CONTAINS: 'contains',
/** Claim is equal to the JSON field's textual content */
EQUALS: 1,
EQUALS: 'equals',
/** Claim is equal to an element of the JSON field's array of strings */
ONEOF: 2
ONEOF: 'oneof'
}
Object.freeze(ClaimRelation)
/**
* Status of the Claim instance
* @readonly
* @enum {number}
*/
export const ClaimStatus = {
/** Claim has been initialized */
INIT: 100,
/** 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
}
/**
* Profile type
* @readonly
* @enum {string}
*/
const ClaimStatus = {
/** Claim has been initialized */
INIT: 'init',
/** Claim has matched its URI to candidate claim definitions */
MATCHED: 'matched',
/** Claim has verified one or multiple candidate claim definitions */
VERIFIED: 'verified'
export const ProfileType = {
/** ASP profile */
ASP: 'asp',
/** OpenPGP profile */
OPENPGP: 'openpgp'
}
Object.freeze(ClaimStatus)
exports.ProxyPolicy = ProxyPolicy
exports.Fetcher = Fetcher
exports.ProofAccess = ProofAccess
exports.ProofFormat = ProofFormat
exports.ClaimFormat = ClaimFormat
exports.ClaimRelation = ClaimRelation
exports.ClaimStatus = ClaimStatus
/**
* 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'
}

104
src/fetcher/activitypub.js Normal file
View file

@ -0,0 +1,104 @@
/*
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.
*/
/**
* 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
*/
export const timeout = 5000
/**
* Execute a fetch request
* @function
* @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
*/
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) => {
(async () => {
let isConfigured = false
try {
isURL(opts.claims.activitypub.url)
isConfigured = true
} catch (_) {}
const now = new Date()
const { host, pathname, search } = new URL(data.url)
const headers = {
host,
date: now.toUTCString(),
accept: 'application/activity+json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
}
if (isConfigured && isNode) {
// Generate the signature
const signedString = `(request-target): get ${pathname}${search}\nhost: ${host}\ndate: ${now.toUTCString()}`
const sign = crypto.createSign('SHA256')
sign.write(signedString)
sign.end()
const signatureSig = sign.sign(opts.claims.activitypub.privateKey.replace(/\\n/g, '\n'), 'base64')
headers.signature = `keyId="${opts.claims.activitypub.url}#main-key",headers="(request-target) host date",signature="${signatureSig}",algorithm="rsa-sha256"`
}
axios.get(data.url,
{
headers
})
.then(res => {
return res.data
})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})()
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

90
src/fetcher/aspe.js Normal file
View file

@ -0,0 +1,90 @@
/*
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,59 +13,64 @@ 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'
/**
* The request's timeout value in milliseconds
* @constant {number} timeout
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
module.exports.timeout = 5000
export const timeout = 5000
if (jsEnv.isNode) {
const dns = require('dns')
/**
* Execute a fetch request
* @function
* @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
*/
export async function fn (data, opts) {
if (isBrowser) {
return null
}
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.domain - The targeted domain
* @returns {object}
*/
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 : module.exports.timeout
)
})
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) => {
dns.resolveTxt(data.domain, (err, records) => {
if (err) {
reject(err)
return
const fetchPromise = new Promise((resolve, reject) => {
dns.resolveTxt(data.domain, (err, records) => {
if (err) {
reject(err)
return
}
resolve({
domain: data.domain,
records: {
txt: records
}
resolve({
domain: data.domain,
records: {
txt: records
}
})
})
})
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}
} else {
module.exports.fn = null
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

88
src/fetcher/graphql.js Normal file
View file

@ -0,0 +1,88 @@
/*
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.
*/
/**
* 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
*/
export const timeout = 5000
/**
* Execute a GraphQL query via HTTP request
* @function
* @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
*/
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
}
let jsonData
try {
jsonData = JSON.parse(data.query)
} catch (error) {
reject(new Error('Invalid GraphQL query object'))
}
axios.post(data.url, jsonData, {
headers: {
'Content-Type': 'application/json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
},
validateStatus: function (status) {
return status >= 200 && status < 400
}
})
.then(res => {
resolve(res.data)
})
.catch(e => {
reject(e)
})
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

View file

@ -13,34 +13,42 @@ 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')
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'
/**
* The request's timeout value in milliseconds
* @constant {number} timeout
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
module.exports.timeout = 5000
export const 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
* @returns {object|string}
* @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
*/
module.exports.fn = async (data, opts) => {
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 : module.exports.timeout
data.fetcherTimeout ? data.fetcherTimeout : timeout
)
})
@ -51,11 +59,12 @@ module.exports.fn = async (data, opts) => {
}
switch (data.format) {
case E.ProofFormat.JSON:
case ProofFormat.JSON:
axios.get(data.url, {
headers: {
Accept: 'application/json',
'User-Agent': `doipjs/${require('../../package.json').version}`
// @ts-ignore
'User-Agent': `doipjs/${version}`
},
validateStatus: function (status) {
return status >= 200 && status < 400
@ -68,7 +77,7 @@ module.exports.fn = async (data, opts) => {
reject(e)
})
break
case E.ProofFormat.TEXT:
case ProofFormat.TEXT:
axios.get(data.url, {
validateStatus: function (status) {
return status >= 200 && status < 400
@ -88,8 +97,7 @@ module.exports.fn = async (data, opts) => {
}
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -13,11 +13,13 @@ 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.
*/
exports.dns = require('./dns')
exports.http = require('./http')
exports.irc = require('./irc')
exports.matrix = require('./matrix')
exports.telegram = require('./telegram')
exports.twitter = require('./twitter')
exports.xmpp = require('./xmpp')
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'

View file

@ -0,0 +1,22 @@
/*
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,84 +13,97 @@ 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'
/**
* The request's timeout value in milliseconds
* @constant {number} timeout
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 20000
*/
module.exports.timeout = 20000
export const timeout = 20000
if (jsEnv.isNode) {
const irc = require('irc-upd')
const validator = require('validator')
/**
* Execute a fetch request
* @function
* @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
*/
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
)
})
/**
* 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 {object} opts - Options used to enable the request
* @param {string} opts.claims.irc.nick - The nick to be used by the library to log in
* @returns {object}
*/
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 : module.exports.timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
try {
isAscii(opts.claims.irc.nick)
} catch (err) {
throw new Error(`IRC fetcher was not set up properly (${err.message})`)
}
const fetchPromise = new Promise((resolve, reject) => {
try {
validator.isAscii(opts.claims.irc.nick)
} catch (err) {
throw new Error(`IRC fetcher was not set up properly (${err.message})`)
}
try {
const client = new irc.Client(data.domain, opts.claims.irc.nick, {
port: 6697,
secure: true,
channels: [],
showErrors: false,
debug: false
})
const reKey = /[a-zA-Z0-9\-_]+\s+:\s(openpgp4fpr:.*)/
const reEnd = /End\sof\s.*\staxonomy./
const keys = []
client.addListener('registered', (message) => {
client.send(`PRIVMSG NickServ TAXONOMY ${data.nick}`)
})
client.addListener('notice', (nick, to, text, message) => {
if (reKey.test(text)) {
const match = text.match(reKey)
keys.push(match[1])
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
}
if (reEnd.test(text)) {
client.disconnect()
resolve(keys)
: {
sasl: false
}
})
} catch (error) {
reject(error)
}
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}
} else {
module.exports.fn = null
const client = new irc.Client(data.domain, opts.claims.irc.nick, {
port: 6697,
secure: true,
channels: [],
showErrors: false,
debug: false,
...saslOptions
})
const reKey = /[a-zA-Z0-9\-_]+\s+:\s((?:openpgp4fpr|aspe):.*)/
const reEnd = /End\sof\s.*\staxonomy./
const keys = []
// @ts-ignore
client.addListener('registered', (message) => {
client.send(`PRIVMSG NickServ TAXONOMY ${data.nick}`)
})
// @ts-ignore
client.addListener('notice', (nick, to, text, message) => {
if (reKey.test(text)) {
const match = text.match(reKey)
keys.push(match[1])
}
if (reEnd.test(text)) {
client.disconnect()
resolve(keys)
}
})
} catch (error) {
reject(error)
}
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

View file

@ -13,44 +13,50 @@ 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')
const validator = require('validator')
/**
* 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'
/**
* The request's timeout value in milliseconds
* @constant {number} timeout
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
module.exports.timeout = 5000
export const 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 {object} opts - Options used to enable the request
* @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 {object}
* @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
*/
module.exports.fn = async (data, opts) => {
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 : module.exports.timeout
data.fetcherTimeout ? data.fetcherTimeout : timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
try {
validator.isFQDN(opts.claims.matrix.instance)
validator.isAscii(opts.claims.matrix.accessToken)
isFQDN(opts.claims.matrix.instance)
isAscii(opts.claims.matrix.accessToken)
} catch (err) {
throw new Error(`Matrix fetcher was not set up properly (${err.message})`)
}
@ -58,7 +64,11 @@ module.exports.fn = async (data, opts) => {
const url = `https://${opts.claims.matrix.instance}/_matrix/client/r0/rooms/${data.roomId}/event/${data.eventId}?access_token=${opts.claims.matrix.accessToken}`
axios.get(url,
{
headers: { Accept: 'application/json' }
headers: {
Accept: 'application/json',
// @ts-ignore
'User-Agent': `doipjs/${version}`
}
})
.then(res => {
return res.data
@ -71,8 +81,7 @@ module.exports.fn = async (data, opts) => {
})
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
return result
})
}

131
src/fetcher/openpgp.js Normal file
View file

@ -0,0 +1,131 @@
/*
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,53 @@ 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')
const validator = require('validator')
/**
* 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'
/**
* The single request's timeout value in milliseconds
* This fetcher makes two requests in total
* @constant {number} timeout
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
module.exports.timeout = 5000
export const timeout = 5000
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {string} data.chat - Telegram public chat username
* @param {string} data.user - Telegram user username
* @param {object} opts - Options used to enable the request
* @param {string} opts.claims.telegram.token - The Telegram Bot API token
* @returns {object|string}
* @param {object} data - Data used in the request
* @param {string} data.chat - Telegram public group name (slug)
* @param {string} data.user - Telegram 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
*/
module.exports.fn = async (data, opts) => {
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 : module.exports.timeout
data.fetcherTimeout ? data.fetcherTimeout : timeout
)
})
const apiPromise = (method) => new Promise((resolve, reject) => {
const apiPromise = (/** @type {string} */ method) => new Promise((resolve, reject) => {
try {
validator.isAscii(opts.claims.telegram.token)
isAscii(opts.claims.telegram.token)
} catch (err) {
throw new Error(`Telegram fetcher was not set up properly (${err.message})`)
}
if (!data.chat || !data.user) {
if (!(data.chat && data.user)) {
reject(new Error('Both chat name and user name must be provided'))
return
}
@ -63,7 +68,8 @@ module.exports.fn = async (data, opts) => {
axios.get(url, {
headers: {
Accept: 'application/json',
'User-Agent': `doipjs/${require('../../package.json').version}`
// @ts-ignore
'User-Agent': `doipjs/${version}`
},
validateStatus: (status) => status === 200
})
@ -103,8 +109,7 @@ module.exports.fn = async (data, opts) => {
})
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -1,81 +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.
*/
const axios = require('axios')
const validator = require('validator')
/**
* @module fetcher/twitter
*/
/**
* The request's timeout value in milliseconds
* @constant {number} timeout
*/
module.exports.timeout = 5000
/**
* Execute a fetch request
* @function
* @async
* @param {object} data - Data used in the request
* @param {number|string} data.tweetId - Identifier of the tweet
* @param {object} opts - Options used to enable the request
* @param {string} opts.claims.twitter.bearerToken - The Twitter API's bearer token
* @returns {object}
*/
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 : module.exports.timeout
)
})
const fetchPromise = new Promise((resolve, reject) => {
try {
validator.isAscii(opts.claims.twitter.bearerToken)
} catch (err) {
throw new Error(
`Twitter fetcher was not set up properly (${err.message})`
)
}
axios.get(
`https://api.twitter.com/1.1/statuses/show.json?id=${data.tweetId}&tweet_mode=extended`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${opts.claims.twitter.bearerToken}`
}
}
)
.then(data => {
return data.data
})
.then((data) => {
resolve(data.full_text)
})
.catch((error) => {
reject(error)
})
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}

View file

@ -13,133 +13,177 @@ 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'
/**
* The request's timeout value in milliseconds
* @constant {number} timeout
* Default timeout after which the fetch is aborted
* @constant
* @type {number}
* @default 5000
*/
module.exports.timeout = 5000
export const timeout = 5000
if (jsEnv.isNode) {
const jsdom = require('jsdom')
const { client, xml } = require('@xmpp/client')
const debug = require('@xmpp/debug')
const validator = require('validator')
let xmpp = null
let iqCaller = null
let xmpp = null
let iqCaller = null
const xmppStart = async (service, username, password) => {
return new Promise((resolve, reject) => {
const xmpp = client({
service: service,
username: username,
password: password
})
if (process.env.NODE_ENV !== 'production') {
debug(xmpp, true)
}
const { iqCaller } = xmpp
xmpp.start()
xmpp.on('online', (address) => {
resolve({ xmpp: xmpp, iqCaller: iqCaller })
})
xmpp.on('error', (error) => {
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 {string} data.field - The vCard field to return (should be "note")
* @param {object} opts - Options used to enable the request
* @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 {object}
*/
module.exports.fn = async (data, opts) => {
try {
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})`)
/**
* 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) => {
return new Promise((resolve, reject) => {
const xmpp = client({ ...params })
if (process.env.NODE_ENV !== 'production') {
debug(xmpp, true)
}
if (!xmpp || xmpp.status !== 'online') {
const xmppStartRes = await xmppStart(
opts.claims.xmpp.service,
opts.claims.xmpp.username,
opts.claims.xmpp.password
)
xmpp = xmppStartRes.xmpp
iqCaller = xmppStartRes.iqCaller
}
const response = await iqCaller.request(
xml('iq', { type: 'get', to: data.id }, xml('vCard', 'vcard-temp')),
30 * 1000
)
const vcardRow = response.getChild('vCard', 'vcard-temp').toString()
const dom = new jsdom.JSDOM(vcardRow)
let timeoutHandle
const timeoutPromise = new Promise((resolve, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('Request was timed out')),
data.fetcherTimeout ? data.fetcherTimeout : module.exports.timeout
)
const { iqCaller } = xmpp
xmpp.start()
xmpp.on('online', _ => {
resolve({ xmpp, iqCaller })
})
const fetchPromise = new Promise((resolve, reject) => {
try {
let vcard
switch (data.field.toLowerCase()) {
case 'desc':
case 'note':
vcard = dom.window.document.querySelector('note text')
if (!vcard) {
vcard = dom.window.document.querySelector('note')
}
if (!vcard) {
vcard = dom.window.document.querySelector('DESC')
}
if (vcard) {
vcard = vcard.textContent
} else {
throw new Error('No DESC or NOTE field found in vCard')
}
break
default:
vcard = dom.window.document.querySelector(data).textContent
break
}
xmpp.stop()
resolve(vcard)
} catch (error) {
reject(error)
}
xmpp.on('error', error => {
reject(error)
})
return Promise.race([fetchPromise, timeoutPromise]).then((result) => {
clearTimeout(timeoutHandle)
return result
})
}
} else {
module.exports.fn = null
})
}
/**
* Execute a fetch request
* @function
* @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
*/
export async function fn (data, opts) {
try {
isFQDN(opts.claims.xmpp.service)
isAscii(opts.claims.xmpp.username)
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)
xmpp = xmppStartRes.xmpp
iqCaller = xmppStartRes.iqCaller
}
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) => {
(async () => {
let completed = false
const proofs = []
// Try the ariadne-id pubsub request
if (!completed) {
try {
const response = await iqCaller.request(
xml('iq', { type: 'get', to: data.id }, xml('pubsub', 'http://jabber.org/protocol/pubsub', xml('items', { node: 'http://ariadne.id/protocol/proof' }))),
30 * 1000
)
// Traverse the XML response
response.getChild('pubsub').getChildren('items').forEach(items => {
if (items.attrs.node === 'http://ariadne.id/protocol/proof') {
items.getChildren('item').forEach(item => {
proofs.push(item.getChildText('value'))
})
}
})
resolve(proofs)
completed = true
} catch (_) {}
}
// Try the vcard4 pubsub request [backward compatibility]
if (!completed) {
try {
const response = await iqCaller.request(
xml('iq', { type: 'get', to: data.id }, xml('pubsub', 'http://jabber.org/protocol/pubsub', xml('items', { node: 'urn:xmpp:vcard4', max_items: '1' }))),
30 * 1000
)
// Traverse the XML response
response.getChild('pubsub').getChildren('items').forEach(items => {
if (items.attrs.node === 'urn:xmpp:vcard4') {
items.getChildren('item').forEach(item => {
if (item.attrs.id === 'current') {
const itemVcard = item.getChild('vcard', 'urn:ietf:params:xml:ns:vcard-4.0')
// Find the vCard URLs
itemVcard.getChildren('url').forEach(url => {
proofs.push(url.getChildText('uri'))
})
// Find the vCard notes
itemVcard.getChildren('note').forEach(note => {
proofs.push(note.getChildText('text'))
})
}
})
}
})
resolve(proofs)
completed = true
} catch (_) {}
}
// Try the vcard-temp IQ request [backward compatibility]
if (!completed) {
try {
const response = await iqCaller.request(
xml('iq', { type: 'get', to: data.id }, xml('vCard', 'vcard-temp')),
30 * 1000
)
// Find the vCard URLs
response.getChild('vCard', 'vcard-temp').getChildren('URL').forEach(url => {
proofs.push(url.children[0])
})
// Find the vCard notes
response.getChild('vCard', 'vcard-temp').getChildren('NOTE').forEach(note => {
proofs.push(note.children[0])
})
response.getChild('vCard', 'vcard-temp').getChildren('DESC').forEach(note => {
proofs.push(note.children[0])
})
resolve(proofs)
completed = true
} catch (error) {
reject(error)
}
}
xmpp.stop()
})()
})
return Promise.race([fetchPromise, timeoutPromise]).finally(() => {
clearTimeout(timeoutHandle)
})
}

View file

@ -13,22 +13,23 @@ 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 Claim = require('./claim')
const claimDefinitions = require('./claimDefinitions')
const proofs = require('./proofs')
const keys = require('./keys')
const signatures = require('./signatures')
const enums = require('./enums')
const defaults = require('./defaults')
const utils = require('./utils')
const verifications = require('./verifications')
exports.Claim = Claim
exports.claimDefinitions = claimDefinitions
exports.proofs = proofs
exports.keys = keys
exports.signatures = signatures
exports.enums = enums
exports.defaults = defaults
exports.utils = utils
exports.verifications = verifications
/**
* @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'

View file

@ -13,98 +13,112 @@ 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')
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')
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'
/**
* Functions related to the fetching and handling of keys
* @module keys
* Functions related to OpenPGP Profiles
* @module openpgp
*/
/**
* Fetch a public key using keyservers
* @function
* @param {string} identifier - Fingerprint or email address
* @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver
* @returns {openpgp.PublicKey}
* @param {string} identifier - Fingerprint or email address
* @param {string} [keyserverDomain] - Domain of the keyserver
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @example
* const key1 = doip.keys.fetchHKP('alice@domain.tld');
* const key2 = doip.keys.fetchHKP('123abc123abc');
* const key3 = doip.keys.fetchHKP('123abc123abc', 'pgpkeys.eu');
*/
const fetchHKP = async (identifier, keyserverDomain) => {
const keyserverBaseUrl = keyserverDomain
? `https://${keyserverDomain}`
: 'https://keys.openpgp.org'
export async function fetchHKP (identifier, keyserverDomain = 'keys.openpgp.org') {
const keyserverBaseUrl = `https://${keyserverDomain ?? 'keys.openpgp.org'}`
const hkp = new HKP(keyserverBaseUrl)
const lookupOpts = {
query: identifier
}
const publicKey = await hkp
const publicKeyArmored = await hkp
.lookup(lookupOpts)
.catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
if (!publicKey) {
if (!publicKeyArmored) {
throw new Error('Key does not exist or could not be fetched')
}
return await openpgp.readKey({
armoredKey: publicKey
const publicKey = await readKey({
armoredKey: publicKeyArmored
})
.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 {openpgp.PublicKey}
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @example
* const key = doip.keys.fetchWKD('alice@domain.tld');
*/
const fetchWKD = async (identifier) => {
export async function fetchWKD (identifier) {
const wkd = new WKD()
const lookupOpts = {
email: identifier
}
const publicKey = await wkd
const publicKeyBinary = await wkd
.lookup(lookupOpts)
.catch((error) => {
.catch((/** @type {Error} */ error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
if (!publicKey) {
if (!publicKeyBinary) {
throw new Error('Key does not exist or could not be fetched')
}
return await openpgp.readKey({
binaryKey: publicKey
const publicKey = await readKey({
binaryKey: publicKeyBinary
})
.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
}
/**
* Fetch a public key from Keybase
* @function
* @param {string} username - Keybase username
* @param {string} fingerprint - Fingerprint of key
* @returns {openpgp.PublicKey}
* @param {string} username - Keybase username
* @param {string} fingerprint - Fingerprint of key
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @example
* const key = doip.keys.fetchKeybase('alice', '123abc123abc');
*/
const fetchKeybase = async (username, fingerprint) => {
export async function fetchKeybase (username, fingerprint) {
const keyLink = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
let rawKeyContent
try {
@ -114,29 +128,36 @@ const fetchKeybase = async (username, fingerprint) => {
responseType: 'text'
}
)
.then((response) => {
.then((/** @type {import('axios').AxiosResponse} */ response) => {
if (response.status === 200) {
return response
}
})
.then((response) => response.data)
.then((/** @type {import('axios').AxiosResponse} */ response) => response.data)
} catch (e) {
throw new Error(`Error fetching Keybase key: ${e.message}`)
}
return await openpgp.readKey({
const publicKey = await 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 plaintext data
* Get a public key from armored public key text data
* @function
* @param {string} rawKeyContent - Plaintext ASCII-formatted public key data
* @returns {openpgp.PublicKey}
* @returns {Promise<Profile>} The profile from the armored public key
* @example
* const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
*
@ -146,29 +167,31 @@ const fetchKeybase = async (username, fingerprint) => {
* -----END PGP PUBLIC KEY BLOCK-----`
* const key = doip.keys.fetchPlaintext(plainkey);
*/
const fetchPlaintext = async (rawKeyContent) => {
const publicKey = await openpgp.readKey({
export async function fetchPlaintext (rawKeyContent) {
const publicKey = await readKey({
armoredKey: rawKeyContent
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
return publicKey
const profile = await parsePublicKey(publicKey)
return profile
}
/**
* Fetch a public key using an URI
* @function
* @param {string} uri - URI that defines the location of the key
* @returns {openpgp.PublicKey}
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @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');
*/
const fetchURI = async (uri) => {
if (!validUrl.isUri(uri)) {
export async function fetchURI (uri) {
if (!isUri(uri)) {
throw new Error('Invalid URI')
}
@ -207,115 +230,103 @@ const fetchURI = async (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 {openpgp.PublicKey}
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
* @example
* const key1 = doip.keys.fetch('alice@domain.tld');
* const key2 = doip.keys.fetch('123abc123abc');
*/
const fetch = async (identifier) => {
export async function fetch (identifier) {
const re = /([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/
const match = identifier.match(re)
let pubKey = null
let profile = null
// Attempt plaintext
if (!pubKey) {
try {
pubKey = await fetchPlaintext(identifier)
} catch (e) {}
}
try {
profile = await fetchPlaintext(identifier)
} catch (e) {}
// Attempt WKD
if (!pubKey && identifier.includes('@')) {
if (!profile && identifier.includes('@')) {
try {
pubKey = await fetchWKD(match[1])
profile = await fetchWKD(match[1])
} catch (e) {}
}
// Attempt HKP
if (!pubKey) {
pubKey = await fetchHKP(
if (!profile) {
profile = await fetchHKP(
match[2] ? match[2] : match[1],
match[2] ? match[1] : null
)
}
if (!pubKey) {
if (!profile) {
throw new Error('Key does not exist or could not be fetched')
}
return pubKey
return profile
}
/**
* Process a public key to get user data and claims
* Process a public key to get a profile
* @function
* @param {openpgp.PublicKey} publicKey - The public key to process
* @returns {object}
* @param {PublicKey} publicKey - The public key to parse
* @returns {Promise<Profile>} The profile from the processed OpenPGP key
* @example
* const key = doip.keys.fetchURI('hkp:alice@domain.tld');
* const data = doip.keys.process(key);
* data.users[0].claims.forEach(claim => {
* const profile = doip.keys.parsePublicKey(key);
* profile.personas[0].claims.forEach(claim => {
* console.log(claim.uri);
* });
*/
const process = async (publicKey) => {
if (!publicKey || !(publicKey instanceof openpgp.PublicKey)) {
export async function parsePublicKey (publicKey) {
if (!(publicKey && (publicKey instanceof PublicKey))) {
throw new Error('Invalid public key')
}
const fingerprint = publicKey.getFingerprint()
const primaryUser = await publicKey.getPrimaryUser()
const users = publicKey.users
const usersOutput = []
const personas = []
users.forEach((user, i) => {
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 (!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)
if ('selfCertifications' in user && user.selfCertifications.length > 0) {
const selfCertification = user.selfCertifications[0]
const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0]
if (selfCertification.revoked) {
pe.revoke()
}
const notations = selfCertification.rawNotations
usersOutput[i].claims = notations
pe.claims = notations
.filter(
({ name, humanReadable }) =>
humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
)
.map(
({ value }) =>
new Claim(new TextDecoder().decode(value), fingerprint)
new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
)
usersOutput[i].userData.isRevoked = selfCertification.revoked
}
personas.push(pe)
})
return {
fingerprint: fingerprint,
users: usersOutput,
primaryUserIndex: primaryUser.index,
key: {
data: publicKey,
fetchMethod: null,
uri: null
}
}
}
const profile = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, personas)
profile.primaryPersonaIndex = primaryUser.index
exports.fetchHKP = fetchHKP
exports.fetchWKD = fetchWKD
exports.fetchKeybase = fetchKeybase
exports.fetchPlaintext = fetchPlaintext
exports.fetchURI = fetchURI
exports.fetch = fetch
exports.process = process
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
}

203
src/persona.js Normal file
View file

@ -0,0 +1,203 @@
/*
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 { Claim } from './claim.js'
/**
* @class
* @classdesc A persona with identity claims
* @example
* const claim = Claim('https://alice.tld', '123');
* const pers = Persona('Alice', 'About Alice', [claim]);
*/
export class Persona {
/**
* @param {string} name - Name of the persona
* @param {Array<Claim>} claims - Claims of the persona
*/
constructor (name, claims) {
/**
* Identifier of the persona
* @type {string | null}
* @public
*/
this.identifier = null
/**
* 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}
* @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
/**
* 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
}

176
src/profile.js Normal file
View file

@ -0,0 +1,176 @@
/*
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 { PublicKeyFetchMethod, PublicKeyEncoding, PublicKeyType, ProfileType } from './enums.js'
import { Persona } from './persona.js'
/**
* @class
* @classdesc A profile of personas with identity claims
* @param {Array<Persona>} personas - Personas of the profile
* @example
* const claim = Claim('https://alice.tld', '123');
* const pers = Persona('Alice', 'About Alice', [claim]);
* const profile = Profile([pers]);
*/
export 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
* @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
/**
* List of personas
* @type {Array<Persona>}
* @public
*/
this.personas = personas || []
/**
* Index of primary persona (to be displayed first or prominently)
* @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
}
}
}
/**
* @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
}

View file

@ -13,10 +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.
*/
const jsEnv = require('browser-or-node')
const fetcher = require('./fetcher')
const utils = require('./utils')
const E = require('./enums')
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'
/**
* @module proofs
@ -28,40 +29,35 @@ const E = require('./enums')
* 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.
* @async
* @param {object} data - Data from a claim definition
* @param {object} opts - Options to enable the request
* @returns {Promise<object|string>}
* @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
*/
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) {
export async function fetch (data, opts) {
if (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 E.ProxyPolicy.ALWAYS:
case ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts)
case E.ProxyPolicy.NEVER:
switch (data.proof.request.access) {
case E.ProofAccess.GENERIC:
case E.ProofAccess.GRANTED:
case ProxyPolicy.NEVER:
switch (data.proof.request.accessRestriction) {
case ProofAccessRestriction.NONE:
case ProofAccessRestriction.GRANTED:
return createDefaultRequestPromise(data, opts)
case E.ProofAccess.NOCORS:
case E.ProofAccess.SERVER:
case ProofAccessRestriction.NOCORS:
case ProofAccessRestriction.SERVER:
throw new Error(
'Impossible to fetch proof (bad combination of service access and proxy policy)'
)
@ -69,15 +65,15 @@ const handleBrowserRequests = (data, opts) => {
throw new Error('Invalid proof access value')
}
case E.ProxyPolicy.ADAPTIVE:
switch (data.proof.request.access) {
case E.ProofAccess.GENERIC:
case ProxyPolicy.ADAPTIVE:
switch (data.proof.request.accessRestriction) {
case ProofAccessRestriction.NONE:
return createFallbackRequestPromise(data, opts)
case E.ProofAccess.NOCORS:
case ProofAccessRestriction.NOCORS:
return createProxyRequestPromise(data, opts)
case E.ProofAccess.GRANTED:
case ProofAccessRestriction.GRANTED:
return createFallbackRequestPromise(data, opts)
case E.ProofAccess.SERVER:
case ProofAccessRestriction.SERVER:
return createProxyRequestPromise(data, opts)
default:
throw new Error('Invalid proof access value')
@ -88,15 +84,20 @@ 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 E.ProxyPolicy.ALWAYS:
case ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts)
case E.ProxyPolicy.NEVER:
case ProxyPolicy.NEVER:
return createDefaultRequestPromise(data, opts)
case E.ProxyPolicy.ADAPTIVE:
case ProxyPolicy.ADAPTIVE:
return createFallbackRequestPromise(data, opts)
default:
@ -104,14 +105,22 @@ 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) => {
return resolve({
fetcher: data.proof.request.fetcher,
data: data,
data,
viaProxy: false,
result: res
})
@ -122,11 +131,16 @@ 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 = utils.generateProxyURL(
proxyUrl = generateProxyURL(
data.proof.request.fetcher,
data.proof.request.data,
opts
@ -137,15 +151,15 @@ const createProxyRequestPromise = (data, opts) => {
const requestData = {
url: proxyUrl,
format: data.proof.request.format,
fetcherTimeout: fetcher[data.proof.request.fetcher].timeout
format: data.proof.response.format,
fetcherTimeout: data.proof.request.fetcher in fetcher ? fetcher[data.proof.request.fetcher].timeout : 30000
}
fetcher.http
.fn(requestData, opts)
.then((res) => {
return resolve({
fetcher: 'http',
data: data,
data,
viaProxy: true,
result: res
})
@ -156,6 +170,11 @@ 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)
@ -173,5 +192,3 @@ const createFallbackRequestPromise = (data, opts) => {
})
})
}
exports.fetch = fetch

View file

@ -1,337 +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.
*/
const router = require('express').Router()
const dns = require('dns')
const axios = require('axios')
const validUrl = require('valid-url')
const jsdom = require('jsdom')
const { client, xml } = require('@xmpp/client')
const debug = require('@xmpp/debug')
const irc = require('irc-upd')
require('dotenv').config()
const xmppService = process.env.XMPP_SERVICE || null
const xmppUsername = process.env.XMPP_USERNAME || null
const xmppPassword = process.env.XMPP_PASSWORD || null
const twitterBearerToken = process.env.TWITTER_BEARER_TOKEN || null
const matrixInstance = process.env.MATRIX_INSTANCE || null
const matrixAccessToken = process.env.MATRIX_ACCESS_TOKEN || null
const ircNick = process.env.IRC_NICK || null
let xmpp = null
let iqCaller = null
let xmppEnabled = true
let twitterEnabled = false
let matrixEnabled = false
let ircEnabled = false
if (!xmppService || !xmppUsername || !xmppPassword) {
xmppEnabled = false
}
if (twitterBearerToken) {
twitterEnabled = true
}
if (matrixInstance && matrixAccessToken) {
matrixEnabled = true
}
if (ircNick) {
ircEnabled = true
}
const xmppStart = async (xmppService, xmppUsername, xmppPassword) => {
return new Promise((resolve, reject) => {
const xmpp = client({
service: xmppService,
username: xmppUsername,
password: xmppPassword
})
if (process.env.NODE_ENV !== 'production') {
debug(xmpp, true)
}
const { iqCaller } = xmpp
xmpp.start()
xmpp.on('online', (address) => {
console.log('online', address.toString())
resolve({ xmpp: xmpp, iqCaller: iqCaller })
})
xmpp.on('error', (error) => {
reject(error)
})
})
}
router.get('/', async (req, res) => {
res.status(200).json({
message:
'Available endpoints: /json/:url, /text/:url, /dns/:hostname, /xmpp/:xmppid, /twitter/:tweetid, /matrix/:roomid/:eventid, /irc/:ircserver/:ircnick'
})
})
router.param('url', async (req, res, next, url) => {
req.params.url = decodeURI(url)
if (!validUrl.isUri(req.params.url)) {
return res.status(400).send({ message: 'URL provided was not valid' })
}
next()
})
router.param('xmppid', async (req, res, next, xmppid) => {
req.params.xmppid = xmppid
if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,})+$/.test(req.params.xmppid)) {
next()
} else {
return res.status(400).json({ message: 'XMPP_ID was not valid' })
}
})
router.param('xmppdata', async (req, res, next, xmppdata) => {
req.params.xmppdata = xmppdata.toUpperCase()
const allowedData = [
'FN',
'NUMBER',
'USERID',
'URL',
'BDAY',
'NICKNAME',
'NOTE',
'DESC'
]
if (!allowedData.includes(req.params.xmppdata)) {
return res.status(400).json({
message:
'Allowed data are: FN, NUMBER, USERID, URL, BDAY, NICKNAME, NOTE, DESC'
})
}
next()
})
router.get('/get/json/:url', (req, res) => {
axios.get(req.params.url,
{
headers: {
Accept: 'application/json'
}
})
.then(result => {
return result.data
})
.then(async (result) => {
return res.status(200).json({ url: req.params.url, content: result })
})
.catch((e) => {
return res.status(400).send({ error: e })
})
})
router.get('/get/text/:url', (req, res) => {
axios.get(req.params.url,
{
responseType: 'text'
})
.then(result => {
return result.data
})
.then(async (result) => {
return res.status(200).json({ url: req.params.url, content: result })
})
.catch((e) => {
return res.status(400).send({ error: e })
})
})
router.get('/get/dns/:hostname', async (req, res) => {
dns.resolveTxt(req.params.hostname, (err, records) => {
if (err) {
throw new Error(err)
}
const out = {
hostname: req.params.hostname,
records: {
txt: records
}
}
return res.status(200).json(out)
})
})
router.get('/get/xmpp/:xmppid', async (req, res) => {
return res
.status(400)
.json(
'Data request parameter missing (FN, NUMBER, USERID, URL, BDAY, NICKNAME, NOTE, DESC)'
)
})
router.get('/get/xmpp/:xmppid/:xmppdata', async (req, res) => {
if (!xmppEnabled) {
return res.status(500).json('XMPP not enabled on server')
}
if (!xmpp) {
const xmppStartRes = await xmppStart(
xmppService,
xmppUsername,
xmppPassword
)
xmpp = xmppStartRes.xmpp
iqCaller = xmppStartRes.iqCaller
}
const response = await iqCaller.request(
xml(
'iq',
{ type: 'get', to: req.params.xmppid },
xml('vCard', 'vcard-temp')
),
30 * 1000
)
const vcardRow = response.getChild('vCard', 'vcard-temp').toString()
const dom = new jsdom.JSDOM(vcardRow)
try {
let vcard
switch (req.params.xmppdata.toLowerCase()) {
case 'desc':
case 'note':
vcard = dom.window.document.querySelector('note text')
if (!vcard) {
vcard = dom.window.document.querySelector('DESC')
}
if (vcard) {
vcard = vcard.textContent
} else {
throw new Error('No DESC or NOTE field found in vCard')
}
break
default:
vcard = dom.window.document.querySelector(req.params.xmppdata)
.textContent
break
}
return res.status(200).json(vcard)
} catch (error) {
return res
.status(400)
.json({ message: 'Request could not be fulfilled', error: error })
}
})
router.get('/get/twitter/:tweetid', async (req, res) => {
if (!twitterEnabled) {
return res.status(500).json('Twitter not enabled on server')
}
axios.get(
`https://api.twitter.com/1.1/statuses/show.json?id=${req.params.tweetid}`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${twitterBearerToken}`
}
}
)
.then(data => {
return data.data
})
.then((data) => {
return res.status(200).json({ data: data, message: 'Success', error: {} })
})
.catch((error) => {
return res.status(error.statusCode || 400).json({
data: [],
message: 'Request could not be fulfilled',
error: error
})
})
})
router.get('/get/matrix/:matrixroomid/:matrixeventid', async (req, res) => {
if (!matrixEnabled) {
return res.status(500).json('Matrix not enabled on server')
}
const url = `https://${matrixInstance}/_matrix/client/r0/rooms/${req.params.matrixroomid}/event/${req.params.matrixeventid}?access_token=${matrixAccessToken}`
axios.get(url,
{
headers: {
Accept: 'application/json'
}
})
.then(data => {
return data.data
})
.then((data) => {
return res.status(200).json({ data: data, message: 'Success', error: {} })
})
.catch((error) => {
return res.status(error.statusCode || 400).json({
data: [],
message: 'Request could not be fulfilled',
error: error
})
})
})
router.get('/get/irc/:ircserver/:ircnick', async (req, res) => {
if (!ircEnabled) {
return res.status(500).json('IRC not enabled on server')
}
try {
const client = new irc.Client(req.params.ircserver, ircNick, {
port: 6697,
secure: true,
channels: []
})
const reKey = /[a-zA-Z0-9\-_]+\s+:\s(openpgp4fpr:.*)/
const reEnd = /End\sof\s.*\staxonomy./
const keys = []
client.addListener('registered', (message) => {
client.send(`PRIVMSG NickServ :TAXONOMY ${req.params.ircnick}`)
})
client.addListener('notice', (nick, to, text, message) => {
if (reKey.test(text)) {
const match = text.match(reKey)
keys.push(match[1])
}
if (reEnd.test(text)) {
client.disconnect()
return res
.status(200)
.json({ data: keys, message: 'Success', error: {} })
}
})
} catch (error) {
return res.status(400).json({
data: [],
message: 'Request could not be fulfilled',
error: error
})
}
})
module.exports = router

View file

@ -1,249 +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.
*/
const router = require('express').Router()
const { query, validationResult } = require('express-validator')
const fetcher = require('../../../fetcher')
const E = require('../../../enums')
require('dotenv').config()
const opts = {
claims: {
irc: {
nick: process.env.IRC_NICK || null
},
matrix: {
instance: process.env.MATRIX_INSTANCE || null,
accessToken: process.env.MATRIX_ACCESS_TOKEN || null
},
telegram: {
token: process.env.TELEGRAM_TOKEN || null
},
xmpp: {
service: process.env.XMPP_SERVICE || null,
username: process.env.XMPP_USERNAME || null,
password: process.env.XMPP_PASSWORD || null
},
twitter: {
bearerToken: process.env.TWITTER_BEARER_TOKEN || null
}
}
}
// Root route
router.get('/', async (req, res) => {
return res.status(400).json({ errors: 'Invalid endpoint' })
})
// HTTP route
router.get(
'/get/http',
query('url').isURL(),
query('format').isIn([E.ProofFormat.JSON, E.ProofFormat.TEXT]),
(req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.http
.fn(req.query, opts)
.then((result) => {
switch (req.query.format) {
case E.ProofFormat.JSON:
return res.status(200).json(result)
case E.ProofFormat.TEXT:
return res.status(200).send(result)
}
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
}
)
// DNS route
router.get('/get/dns', query('domain').isFQDN(), (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.dns
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
})
// XMPP route
router.get(
'/get/xmpp',
query('id').isEmail(),
query('field').isIn([
'fn',
'number',
'userid',
'url',
'bday',
'nickname',
'note',
'desc'
]),
async (req, res) => {
if (
!opts.claims.xmpp.service ||
!opts.claims.xmpp.username ||
!opts.claims.xmpp.password
) {
return res.status(501).json({ errors: 'XMPP not enabled on server' })
}
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.xmpp
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
}
)
// Twitter route
router.get('/get/twitter', query('tweetId').isInt(), async (req, res) => {
if (!opts.claims.twitter.bearerToken) {
return res.status(501).json({ errors: 'Twitter not enabled on server' })
}
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.twitter
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
})
// Matrix route
router.get(
'/get/matrix',
query('roomId').isString(),
query('eventId').isString(),
async (req, res) => {
if (!opts.claims.matrix.instance || !opts.claims.matrix.accessToken) {
return res.status(501).json({ errors: 'Matrix not enabled on server' })
}
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.matrix
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
}
)
// Telegram route
router.get(
'/get/telegram',
query('user').isString(),
query('chat').isString(),
async (req, res) => {
if (!opts.claims.telegram.token) {
return res.status(501).json({ errors: 'Telegram not enabled on server' })
}
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.telegram
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
}
)
// IRC route
router.get('/get/irc', query('nick').isString(), async (req, res) => {
if (!opts.claims.irc.nick) {
return res.status(501).json({ errors: 'IRC not enabled on server' })
}
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.irc
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
})
// Gitlab route
router.get(
'/get/gitlab',
query('domain').isFQDN(),
query('username').isString(),
async (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.http
.fn({
url: `https://${req.query.domain}/api/v4/projects/${req.query.username}%2Fgitlab_proof`,
format: 'json'
}, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
}
)
module.exports = router

View file

@ -1,56 +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.
*/
/*
Copyright 2020 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 express = require('express')
const app = express()
const cors = require('cors')
require('dotenv').config()
app.use(cors())
app.set('port', process.env.PORT || 3000)
app.use('/api/1', require('./api/v1/'))
app.use('/api/2', require('./api/v2/'))
app.get('/', (req, res) => {
return res.status(200).json({ message: 'Available endpoints: /api' })
})
app.get('/api', (req, res) => {
return res
.status(200)
.json({ message: 'Available API versions: /api/1, /api/2' })
})
app.all('*', (req, res) => {
return res.status(404).json({ message: 'API endpoint not found' })
})
app.listen(app.get('port'), () => {
console.log(`Node server listening at http://localhost:${app.get('port')}`)
})

373
src/schemas.js Normal file
View file

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

62
src/serviceProvider.js Normal file
View file

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

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

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

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

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

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

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

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

@ -0,0 +1,92 @@
/*
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.
*/
/**
* 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');
*/
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: 'gitea',
name: 'Gitea',
homepage: 'https://about.gitea.com'
},
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 tests = [
{
uri: 'https://domain.org/alice/gitea_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/gitea_proof/',
shouldMatch: true
},
{
uri: 'https://domain.org/alice/other_proof',
shouldMatch: true
},
{
uri: 'https://domain.org/alice',
shouldMatch: false
}
]

View file

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

@ -13,48 +13,65 @@ 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')
/**
* 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');
*/
const reURI = /^https:\/\/(.*)\/(.*)\/gitlab_proof\/?/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
const processURI = (uri) => {
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 {
serviceprovider: {
type: 'web',
name: 'gitlab'
},
match: {
regularExpression: reURI,
isAmbiguous: true
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: {
uri: uri,
request: {
fetcher: E.Fetcher.HTTP,
access: E.ProofAccess.GENERIC,
format: E.ProofFormat.JSON,
accessRestriction: E.ProofAccessRestriction.NONE,
data: {
url: `https://${match[1]}/api/v4/projects/${match[2]}%2Fgitlab_proof`,
format: E.ProofFormat.JSON
}
}
},
claim: {
format: E.ClaimFormat.URI,
relation: E.ClaimRelation.EQUALS,
path: ['description']
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.EQUALS,
path: ['description']
}]
}
}
})
}
const tests = [
export const tests = [
{
uri: 'https://gitlab.domain.org/alice/gitlab_proof',
shouldMatch: true
@ -68,7 +85,3 @@ const tests = [
shouldMatch: false
}
]
exports.reURI = reURI
exports.processURI = processURI
exports.tests = tests

View file

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

@ -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.
*/
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

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

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

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

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

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

@ -0,0 +1,114 @@
/*
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.
*/
/**
* 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/...');
*/
import * as E from '../enums.js'
import { ServiceProvider } from '../serviceProvider.js'
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 match = uri.match(reURI)
if (!match[2]) {
return null
}
const params = new URLSearchParams(match[2])
if (!(params.has('org.keyoxide.e') && params.has('org.keyoxide.r'))) {
return null
}
const paramRoomId = `${params.get('org.keyoxide.r')[0] !== '!' ? '!' : ''}${params.get('org.keyoxide.r')}`
const paramEventId = `${params.get('org.keyoxide.e')[0] !== '$' ? '$' : ''}${params.get('org.keyoxide.e')}`
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'
},
profile: {
display: `@${match[1]}`,
uri: profileUrl,
qr: null
},
claim: {
uriRegularExpression: reURI.toString(),
uriIsAmbiguous: false
},
proof: {
request: {
uri: eventUrl,
fetcher: E.Fetcher.MATRIX,
accessRestriction: E.ProofAccessRestriction.GRANTED,
data: {
eventId: paramEventId,
roomId: paramRoomId
}
},
response: {
format: E.ProofFormat.JSON
},
target: [{
format: E.ClaimFormat.URI,
encoding: E.EntityEncodingFormat.PLAIN,
relation: E.ClaimRelation.CONTAINS,
path: ['content', 'body']
}]
}
})
}
export const tests = [
{
uri:
'matrix:u/alice:matrix.domain.org?org.keyoxide.r=123:domain.org&org.keyoxide.e=123',
shouldMatch: true
},
{
uri: 'matrix:u/alice:matrix.domain.org',
shouldMatch: true
},
{
uri:
'matrix:u/@alice:matrix.domain.org?org.keyoxide.r=!123:domain.org&org.keyoxide.e=$123',
shouldMatch: true
},
{
uri: 'xmpp:alice@domain.org',
shouldMatch: false
},
{
uri: 'https://domain.org/@alice',
shouldMatch: false
}
]

View file

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

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