@keytrace/runner 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/actions/css-select.d.ts +6 -0
- package/dist/actions/css-select.d.ts.map +1 -0
- package/dist/actions/css-select.js +14 -0
- package/dist/actions/css-select.js.map +1 -0
- package/dist/actions/dns-txt.d.ts +6 -0
- package/dist/actions/dns-txt.d.ts.map +1 -0
- package/dist/actions/dns-txt.js +17 -0
- package/dist/actions/dns-txt.js.map +1 -0
- package/dist/actions/http-get.d.ts +6 -0
- package/dist/actions/http-get.d.ts.map +1 -0
- package/dist/actions/http-get.js +19 -0
- package/dist/actions/http-get.js.map +1 -0
- package/dist/actions/index.d.ts +6 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +6 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/json-path.d.ts +12 -0
- package/dist/actions/json-path.d.ts.map +1 -0
- package/dist/actions/json-path.js +26 -0
- package/dist/actions/json-path.js.map +1 -0
- package/dist/actions/regex-match.d.ts +6 -0
- package/dist/actions/regex-match.d.ts.map +1 -0
- package/dist/actions/regex-match.js +14 -0
- package/dist/actions/regex-match.js.map +1 -0
- package/dist/claim.d.ts +38 -0
- package/dist/claim.d.ts.map +1 -0
- package/dist/claim.js +253 -0
- package/dist/claim.js.map +1 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +17 -0
- package/dist/constants.js.map +1 -0
- package/dist/expect.d.ts +12 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +33 -0
- package/dist/expect.js.map +1 -0
- package/dist/fetchers/activitypub.d.ts +25 -0
- package/dist/fetchers/activitypub.d.ts.map +1 -0
- package/dist/fetchers/activitypub.js +32 -0
- package/dist/fetchers/activitypub.js.map +1 -0
- package/dist/fetchers/dns.d.ts +21 -0
- package/dist/fetchers/dns.d.ts.map +1 -0
- package/dist/fetchers/dns.js +61 -0
- package/dist/fetchers/dns.js.map +1 -0
- package/dist/fetchers/http.d.ts +10 -0
- package/dist/fetchers/http.d.ts.map +1 -0
- package/dist/fetchers/http.js +30 -0
- package/dist/fetchers/http.js.map +1 -0
- package/dist/fetchers/index.d.ts +16 -0
- package/dist/fetchers/index.d.ts.map +1 -0
- package/dist/fetchers/index.js +22 -0
- package/dist/fetchers/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolate.d.ts +12 -0
- package/dist/interpolate.d.ts.map +1 -0
- package/dist/interpolate.js +23 -0
- package/dist/interpolate.js.map +1 -0
- package/dist/profile.d.ts +42 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +176 -0
- package/dist/profile.js.map +1 -0
- package/dist/recipes/dns-txt.d.ts +9 -0
- package/dist/recipes/dns-txt.d.ts.map +1 -0
- package/dist/recipes/dns-txt.js +45 -0
- package/dist/recipes/dns-txt.js.map +1 -0
- package/dist/recipes/github-gist.d.ts +9 -0
- package/dist/recipes/github-gist.d.ts.map +1 -0
- package/dist/recipes/github-gist.js +52 -0
- package/dist/recipes/github-gist.js.map +1 -0
- package/dist/recipes/index.d.ts +3 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +3 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/runner.d.ts +7 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +100 -0
- package/dist/runner.js.map +1 -0
- package/dist/serviceProviders/activitypub.d.ts +10 -0
- package/dist/serviceProviders/activitypub.d.ts.map +1 -0
- package/dist/serviceProviders/activitypub.js +73 -0
- package/dist/serviceProviders/activitypub.js.map +1 -0
- package/dist/serviceProviders/bsky.d.ts +10 -0
- package/dist/serviceProviders/bsky.d.ts.map +1 -0
- package/dist/serviceProviders/bsky.js +63 -0
- package/dist/serviceProviders/bsky.js.map +1 -0
- package/dist/serviceProviders/dns.d.ts +10 -0
- package/dist/serviceProviders/dns.d.ts.map +1 -0
- package/dist/serviceProviders/dns.js +65 -0
- package/dist/serviceProviders/dns.js.map +1 -0
- package/dist/serviceProviders/github.d.ts +10 -0
- package/dist/serviceProviders/github.d.ts.map +1 -0
- package/dist/serviceProviders/github.js +100 -0
- package/dist/serviceProviders/github.js.map +1 -0
- package/dist/serviceProviders/index.d.ts +26 -0
- package/dist/serviceProviders/index.d.ts.map +1 -0
- package/dist/serviceProviders/index.js +55 -0
- package/dist/serviceProviders/index.js.map +1 -0
- package/dist/serviceProviders/npm.d.ts +10 -0
- package/dist/serviceProviders/npm.d.ts.map +1 -0
- package/dist/serviceProviders/npm.js +99 -0
- package/dist/serviceProviders/npm.js.map +1 -0
- package/dist/serviceProviders/types.d.ts +106 -0
- package/dist/serviceProviders/types.d.ts.map +1 -0
- package/dist/serviceProviders/types.js +2 -0
- package/dist/serviceProviders/types.js.map +1 -0
- package/dist/types.d.ts +165 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
- package/src/actions/css-select.ts +14 -0
- package/src/actions/dns-txt.ts +16 -0
- package/src/actions/http-get.ts +19 -0
- package/src/actions/index.ts +5 -0
- package/src/actions/json-path.ts +29 -0
- package/src/actions/regex-match.ts +13 -0
- package/src/claim.ts +293 -0
- package/src/constants.ts +19 -0
- package/src/expect.ts +36 -0
- package/src/fetchers/activitypub.ts +53 -0
- package/src/fetchers/dns.ts +82 -0
- package/src/fetchers/http.ts +38 -0
- package/src/fetchers/index.ts +30 -0
- package/src/index.ts +57 -0
- package/src/interpolate.ts +20 -0
- package/src/profile.ts +229 -0
- package/src/recipes/dns-txt.ts +46 -0
- package/src/recipes/github-gist.ts +53 -0
- package/src/recipes/index.ts +2 -0
- package/src/runner.ts +116 -0
- package/src/serviceProviders/activitypub.ts +84 -0
- package/src/serviceProviders/bsky.ts +73 -0
- package/src/serviceProviders/dns.ts +75 -0
- package/src/serviceProviders/github.ts +112 -0
- package/src/serviceProviders/index.ts +65 -0
- package/src/serviceProviders/npm.ts +116 -0
- package/src/serviceProviders/types.ts +121 -0
- package/src/types.ts +181 -0
package/src/runner.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Recipe, ClaimContext, RunnerConfig, VerificationResult, StepResult, VerificationStep, FetchFn } from "./types.js";
|
|
2
|
+
import { interpolate } from "./interpolate.js";
|
|
3
|
+
import { checkExpect } from "./expect.js";
|
|
4
|
+
import { httpGet } from "./actions/http-get.js";
|
|
5
|
+
import { jsonPath } from "./actions/json-path.js";
|
|
6
|
+
import { cssSelect } from "./actions/css-select.js";
|
|
7
|
+
import { regexMatch } from "./actions/regex-match.js";
|
|
8
|
+
import { dnsTxt } from "./actions/dns-txt.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT = 10_000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Execute a recipe's verification steps against a claim context.
|
|
14
|
+
* Returns a full result with per-step details. Stops on first failure.
|
|
15
|
+
*/
|
|
16
|
+
export async function runRecipe(recipe: Recipe, context: ClaimContext, config?: RunnerConfig): Promise<VerificationResult> {
|
|
17
|
+
const fetchFn: FetchFn = config?.fetch ?? globalThis.fetch;
|
|
18
|
+
const timeout = config?.timeout ?? DEFAULT_TIMEOUT;
|
|
19
|
+
const steps: StepResult[] = [];
|
|
20
|
+
|
|
21
|
+
// Extract subject from params if a param defines extractFrom
|
|
22
|
+
let subject: string | undefined;
|
|
23
|
+
if (recipe.params) {
|
|
24
|
+
for (const param of recipe.params) {
|
|
25
|
+
if (param.extractFrom && context.params[param.key]) {
|
|
26
|
+
const regex = new RegExp(param.extractFrom);
|
|
27
|
+
const match = context.params[param.key].match(regex);
|
|
28
|
+
if (match?.[1]) {
|
|
29
|
+
subject = `${recipe.type.split("-")[0]}:${match[1]}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Tracks the last "fetch" output (http-get, dns-txt) for extraction steps to use.
|
|
36
|
+
// Extraction steps (json-path, css-select, regex-match) always operate on the
|
|
37
|
+
// last fetch output, not on each other's output.
|
|
38
|
+
let lastFetchOutput: unknown = undefined;
|
|
39
|
+
|
|
40
|
+
for (const step of recipe.verification.steps) {
|
|
41
|
+
const isFetchAction = step.action === "http-get" || step.action === "dns-txt";
|
|
42
|
+
const result = await executeStep(step, context, lastFetchOutput, fetchFn, timeout);
|
|
43
|
+
steps.push(result);
|
|
44
|
+
|
|
45
|
+
if (!result.success) {
|
|
46
|
+
return { success: false, steps, subject, error: result.error };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (isFetchAction) {
|
|
50
|
+
lastFetchOutput = result.data;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { success: true, steps, subject };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function executeStep(step: VerificationStep, context: ClaimContext, previousOutput: unknown, fetchFn: FetchFn, timeout: number): Promise<StepResult> {
|
|
58
|
+
try {
|
|
59
|
+
let data: unknown;
|
|
60
|
+
|
|
61
|
+
switch (step.action) {
|
|
62
|
+
case "http-get": {
|
|
63
|
+
const url = interpolate(step.url!, context);
|
|
64
|
+
data = await httpGet(url, fetchFn, timeout);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
case "json-path": {
|
|
69
|
+
const selector = interpolate(step.selector!, context);
|
|
70
|
+
const input = previousOutput ?? "";
|
|
71
|
+
data = jsonPath(input as string | object, selector);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case "css-select": {
|
|
76
|
+
const selector = interpolate(step.selector!, context);
|
|
77
|
+
const input = previousOutput as string;
|
|
78
|
+
if (typeof input !== "string") {
|
|
79
|
+
throw new Error("css-select requires string input from a previous step");
|
|
80
|
+
}
|
|
81
|
+
data = cssSelect(input, selector);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case "regex-match": {
|
|
86
|
+
const pattern = interpolate(step.pattern!, context);
|
|
87
|
+
const input = typeof previousOutput === "string" ? previousOutput : String(previousOutput ?? "");
|
|
88
|
+
data = regexMatch(input, pattern);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case "dns-txt": {
|
|
93
|
+
const domain = step.url ? interpolate(step.url, context) : interpolate(step.pattern!, context);
|
|
94
|
+
data = await dnsTxt(domain);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`Unknown action: "${step.action}"`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check expect if defined
|
|
103
|
+
if (step.expect) {
|
|
104
|
+
const expectStr = interpolate(step.expect, context);
|
|
105
|
+
const result = checkExpect(expectStr, data);
|
|
106
|
+
if (!result.pass) {
|
|
107
|
+
return { action: step.action, success: false, data, error: result.message };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { action: step.action, success: true, data };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
return { action: step.action, success: false, error: message };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ServiceProvider } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ActivityPub (Mastodon/Fediverse) service provider
|
|
5
|
+
*
|
|
6
|
+
* Users prove ownership by adding their DID to their profile bio or fields.
|
|
7
|
+
* The claim URI is the profile URL (e.g., https://mastodon.social/@username)
|
|
8
|
+
*/
|
|
9
|
+
const activitypub: ServiceProvider = {
|
|
10
|
+
id: "activitypub",
|
|
11
|
+
name: "Mastodon",
|
|
12
|
+
homepage: "https://joinmastodon.org",
|
|
13
|
+
|
|
14
|
+
// Match Mastodon-style profile URLs: https://instance/@username
|
|
15
|
+
reUri: /^https:\/\/([^/]+)\/@([^/]+)\/?$/,
|
|
16
|
+
|
|
17
|
+
// Could match other ActivityPub software with same URL pattern
|
|
18
|
+
isAmbiguous: true,
|
|
19
|
+
|
|
20
|
+
ui: {
|
|
21
|
+
description: "Link your Mastodon or Fediverse account",
|
|
22
|
+
icon: "at-sign",
|
|
23
|
+
inputLabel: "Profile URL",
|
|
24
|
+
inputPlaceholder: "https://mastodon.social/@username",
|
|
25
|
+
instructions: [
|
|
26
|
+
"Go to your Mastodon instance and open **Edit profile**",
|
|
27
|
+
"Add your DID to your **bio** or create a new **profile metadata field**",
|
|
28
|
+
"For metadata fields, set the label to `keytrace` and paste your DID as the value",
|
|
29
|
+
"Save your profile changes",
|
|
30
|
+
"Paste your full profile URL below (e.g., `https://mastodon.social/@username`)",
|
|
31
|
+
],
|
|
32
|
+
proofTemplate: "{did}",
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
processURI(uri, match) {
|
|
36
|
+
const [, domain, username] = match;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
profile: {
|
|
40
|
+
display: `@${username}@${domain}`,
|
|
41
|
+
uri,
|
|
42
|
+
},
|
|
43
|
+
proof: {
|
|
44
|
+
request: {
|
|
45
|
+
uri,
|
|
46
|
+
fetcher: "activitypub",
|
|
47
|
+
format: "json",
|
|
48
|
+
},
|
|
49
|
+
target: [
|
|
50
|
+
// Check profile bio/summary (HTML content)
|
|
51
|
+
{ path: ["summary"], relation: "contains", format: "text" },
|
|
52
|
+
// Check profile fields (Mastodon-style verification fields)
|
|
53
|
+
{ path: ["attachment", "*", "value"], relation: "contains", format: "text" },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
postprocess(data) {
|
|
60
|
+
const actor = data as { preferredUsername?: string; name?: string; icon?: { url?: string } };
|
|
61
|
+
return {
|
|
62
|
+
displayName: actor.name || actor.preferredUsername,
|
|
63
|
+
avatarUrl: actor.icon?.url,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
getProofText(did) {
|
|
68
|
+
return did;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
getProofLocation() {
|
|
72
|
+
return `Add to your profile bio or a profile metadata field`;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
tests: [
|
|
76
|
+
{ uri: "https://mastodon.social/@alice", shouldMatch: true },
|
|
77
|
+
{ uri: "https://fosstodon.org/@bob/", shouldMatch: true },
|
|
78
|
+
{ uri: "https://hachyderm.io/@user", shouldMatch: true },
|
|
79
|
+
{ uri: "https://twitter.com/alice", shouldMatch: false },
|
|
80
|
+
{ uri: "https://mastodon.social/alice", shouldMatch: false },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default activitypub;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ServiceProvider } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bluesky service provider
|
|
5
|
+
*
|
|
6
|
+
* Users prove ownership of another Bluesky account by adding their DID to the profile bio.
|
|
7
|
+
* The claim URI is the bsky.app profile URL.
|
|
8
|
+
*/
|
|
9
|
+
const bsky: ServiceProvider = {
|
|
10
|
+
id: "bsky",
|
|
11
|
+
name: "Bluesky",
|
|
12
|
+
homepage: "https://bsky.app",
|
|
13
|
+
|
|
14
|
+
// Match Bluesky profile URLs: https://bsky.app/profile/handle or did
|
|
15
|
+
reUri: /^https:\/\/bsky\.app\/profile\/([^/]+)\/?$/,
|
|
16
|
+
|
|
17
|
+
isAmbiguous: false,
|
|
18
|
+
|
|
19
|
+
ui: {
|
|
20
|
+
description: "Link another Bluesky account",
|
|
21
|
+
icon: "cloud",
|
|
22
|
+
inputLabel: "Profile URL",
|
|
23
|
+
inputPlaceholder: "https://bsky.app/profile/username.bsky.social",
|
|
24
|
+
instructions: [
|
|
25
|
+
"Log into the Bluesky account you want to link",
|
|
26
|
+
"Go to **Settings** → **Edit Profile**",
|
|
27
|
+
"Add your DID to your **bio** (the verification DID, not this account's DID)",
|
|
28
|
+
"Save your profile changes",
|
|
29
|
+
"Paste the profile URL below",
|
|
30
|
+
],
|
|
31
|
+
proofTemplate: "{did}",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
processURI(uri, match) {
|
|
35
|
+
const [, handle] = match;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
profile: {
|
|
39
|
+
display: handle.startsWith("did:") ? handle : `@${handle}`,
|
|
40
|
+
uri,
|
|
41
|
+
},
|
|
42
|
+
proof: {
|
|
43
|
+
request: {
|
|
44
|
+
uri: `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`,
|
|
45
|
+
fetcher: "http",
|
|
46
|
+
format: "json",
|
|
47
|
+
},
|
|
48
|
+
target: [
|
|
49
|
+
// Check profile description/bio
|
|
50
|
+
{ path: ["description"], relation: "contains", format: "text" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
getProofText(did) {
|
|
57
|
+
return did;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
getProofLocation() {
|
|
61
|
+
return `Add to your profile bio`;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
tests: [
|
|
65
|
+
{ uri: "https://bsky.app/profile/alice.bsky.social", shouldMatch: true },
|
|
66
|
+
{ uri: "https://bsky.app/profile/did:plc:abc123", shouldMatch: true },
|
|
67
|
+
{ uri: "https://bsky.app/profile/alice.bsky.social/", shouldMatch: true },
|
|
68
|
+
{ uri: "https://bsky.app/profile/alice/post/123", shouldMatch: false },
|
|
69
|
+
{ uri: "https://bsky.social/profile/alice", shouldMatch: false },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default bsky;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ServiceProvider } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DNS TXT record service provider
|
|
5
|
+
*
|
|
6
|
+
* Users prove domain ownership by adding a TXT record containing their DID.
|
|
7
|
+
* The claim URI format is: dns:example.com
|
|
8
|
+
*/
|
|
9
|
+
const dns: ServiceProvider = {
|
|
10
|
+
id: "dns",
|
|
11
|
+
name: "Domain",
|
|
12
|
+
homepage: "",
|
|
13
|
+
|
|
14
|
+
// Match dns:domain.tld URIs (must contain at least one dot)
|
|
15
|
+
reUri: /^dns:([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)+)$/,
|
|
16
|
+
|
|
17
|
+
isAmbiguous: false,
|
|
18
|
+
|
|
19
|
+
ui: {
|
|
20
|
+
description: "Link via DNS TXT record",
|
|
21
|
+
icon: "globe",
|
|
22
|
+
inputLabel: "Domain",
|
|
23
|
+
inputPlaceholder: "example.com",
|
|
24
|
+
instructions: [
|
|
25
|
+
"Open your domain's DNS settings (usually in your registrar or hosting provider)",
|
|
26
|
+
"Add a new **TXT record** at the root domain (or at `_keytrace.yourdomain.com`)",
|
|
27
|
+
"Set the record value to the verification content below",
|
|
28
|
+
"Save and wait for DNS propagation (may take a few minutes to an hour)",
|
|
29
|
+
"Enter your domain below and verify",
|
|
30
|
+
],
|
|
31
|
+
proofTemplate: "keytrace-verification={did}",
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
processURI(uri, match) {
|
|
35
|
+
const [, domain] = match;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
profile: {
|
|
39
|
+
display: domain,
|
|
40
|
+
uri: `https://${domain}`,
|
|
41
|
+
},
|
|
42
|
+
proof: {
|
|
43
|
+
request: {
|
|
44
|
+
uri: domain,
|
|
45
|
+
fetcher: "dns",
|
|
46
|
+
format: "json",
|
|
47
|
+
},
|
|
48
|
+
target: [
|
|
49
|
+
// Look for DID in any TXT record
|
|
50
|
+
{ path: ["records", "txt"], relation: "contains", format: "text" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
getProofText(did) {
|
|
57
|
+
return `keytrace-verification=${did}`;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
getProofLocation(match) {
|
|
61
|
+
const [, domain] = match;
|
|
62
|
+
return `Add a TXT record at the root of ${domain} (or at _keytrace.${domain})`;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
tests: [
|
|
66
|
+
{ uri: "dns:example.com", shouldMatch: true },
|
|
67
|
+
{ uri: "dns:sub.example.com", shouldMatch: true },
|
|
68
|
+
{ uri: "dns:a.b.c.example.com", shouldMatch: true },
|
|
69
|
+
{ uri: "dns:example", shouldMatch: false },
|
|
70
|
+
{ uri: "dns:-invalid.com", shouldMatch: false },
|
|
71
|
+
{ uri: "https://example.com", shouldMatch: false },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default dns;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ServiceProvider } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Gist service provider
|
|
5
|
+
*
|
|
6
|
+
* Users prove ownership by creating a public gist containing their DID.
|
|
7
|
+
* The gist URL is used as the claim URI.
|
|
8
|
+
*/
|
|
9
|
+
const github: ServiceProvider = {
|
|
10
|
+
id: "github",
|
|
11
|
+
name: "GitHub",
|
|
12
|
+
homepage: "https://github.com",
|
|
13
|
+
|
|
14
|
+
// Match GitHub Gist URLs: https://gist.github.com/username/gistid
|
|
15
|
+
reUri: /^https:\/\/gist\.github\.com\/([^/]+)\/([a-f0-9]+)\/?$/,
|
|
16
|
+
|
|
17
|
+
isAmbiguous: false,
|
|
18
|
+
|
|
19
|
+
ui: {
|
|
20
|
+
description: "Link via a public gist",
|
|
21
|
+
icon: "github",
|
|
22
|
+
inputLabel: "Gist URL",
|
|
23
|
+
inputPlaceholder: "https://gist.github.com/username/abc123...",
|
|
24
|
+
instructions: [
|
|
25
|
+
"Go to [gist.github.com](https://gist.github.com) and create a new gist",
|
|
26
|
+
"Name the file `keytrace.json` (or `keytrace.md` or `proof.md`)",
|
|
27
|
+
"Paste the verification content below into the file",
|
|
28
|
+
"Make sure the gist is **public**, then save it",
|
|
29
|
+
"Copy the gist URL and paste it below",
|
|
30
|
+
],
|
|
31
|
+
proofTemplate: '{\n "did": "{did}"\n}',
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
processURI(uri, match) {
|
|
35
|
+
const [, username, gistId] = match;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
profile: {
|
|
39
|
+
display: `@${username}`,
|
|
40
|
+
uri: `https://github.com/${username}`,
|
|
41
|
+
},
|
|
42
|
+
proof: {
|
|
43
|
+
request: {
|
|
44
|
+
uri: `https://api.github.com/gists/${gistId}`,
|
|
45
|
+
fetcher: "http",
|
|
46
|
+
format: "json",
|
|
47
|
+
options: {
|
|
48
|
+
headers: {
|
|
49
|
+
Accept: "application/vnd.github.v3+json",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
target: [
|
|
54
|
+
// Check keytrace.json file content
|
|
55
|
+
{
|
|
56
|
+
path: ["files", "keytrace.json", "content"],
|
|
57
|
+
relation: "contains",
|
|
58
|
+
format: "text",
|
|
59
|
+
},
|
|
60
|
+
// Check proof.md file content
|
|
61
|
+
{
|
|
62
|
+
path: ["files", "proof.md", "content"],
|
|
63
|
+
relation: "contains",
|
|
64
|
+
format: "text",
|
|
65
|
+
},
|
|
66
|
+
// Check keytrace.md file content
|
|
67
|
+
{
|
|
68
|
+
path: ["files", "keytrace.md", "content"],
|
|
69
|
+
relation: "contains",
|
|
70
|
+
format: "text",
|
|
71
|
+
},
|
|
72
|
+
// Check openpgp.md for backwards compatibility with Keyoxide
|
|
73
|
+
{
|
|
74
|
+
path: ["files", "openpgp.md", "content"],
|
|
75
|
+
relation: "contains",
|
|
76
|
+
format: "text",
|
|
77
|
+
},
|
|
78
|
+
// Check gist description
|
|
79
|
+
{ path: ["description"], relation: "contains", format: "text" },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
postprocess(data, match) {
|
|
86
|
+
const [, username] = match;
|
|
87
|
+
const gist = data as { owner?: { avatar_url?: string; login?: string } };
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
subject: gist.owner?.login ?? username,
|
|
91
|
+
avatarUrl: gist.owner?.avatar_url,
|
|
92
|
+
profileUrl: `https://github.com/${gist.owner?.login ?? username}`,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getProofText(did) {
|
|
97
|
+
return `Verifying my identity on keytrace: ${did}`;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getProofLocation() {
|
|
101
|
+
return `Create a public gist with a file named keytrace.json, keytrace.md, or proof.md containing the proof text`;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
tests: [
|
|
105
|
+
{ uri: "https://gist.github.com/alice/abc123def456", shouldMatch: true },
|
|
106
|
+
{ uri: "https://gist.github.com/alice/abc123def456/", shouldMatch: true },
|
|
107
|
+
{ uri: "https://github.com/alice", shouldMatch: false },
|
|
108
|
+
{ uri: "https://gist.gitlab.com/alice/abc123", shouldMatch: false },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default github;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import github from "./github.js";
|
|
2
|
+
import dns from "./dns.js";
|
|
3
|
+
import activitypub from "./activitypub.js";
|
|
4
|
+
import bsky from "./bsky.js";
|
|
5
|
+
import npm from "./npm.js";
|
|
6
|
+
import type { ServiceProvider, ServiceProviderMatch } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export type { ServiceProvider, ServiceProviderMatch, ServiceProviderUI, ProofTarget, ProofRequest, ProcessedURI } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const providers: Record<string, ServiceProvider> = {
|
|
11
|
+
github,
|
|
12
|
+
dns,
|
|
13
|
+
activitypub,
|
|
14
|
+
bsky,
|
|
15
|
+
npm,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get a service provider by ID
|
|
20
|
+
*/
|
|
21
|
+
export function getProvider(id: string): ServiceProvider | undefined {
|
|
22
|
+
return providers[id];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get all registered service providers
|
|
27
|
+
*/
|
|
28
|
+
export function getAllProviders(): ServiceProvider[] {
|
|
29
|
+
return Object.values(providers);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Match a URI against all service providers
|
|
34
|
+
* Returns all matching providers, with unambiguous matches stopping the search
|
|
35
|
+
*/
|
|
36
|
+
export function matchUri(uri: string): ServiceProviderMatch[] {
|
|
37
|
+
const matches: ServiceProviderMatch[] = [];
|
|
38
|
+
|
|
39
|
+
for (const provider of Object.values(providers)) {
|
|
40
|
+
const match = uri.match(provider.reUri);
|
|
41
|
+
if (match) {
|
|
42
|
+
matches.push({
|
|
43
|
+
provider,
|
|
44
|
+
match,
|
|
45
|
+
isAmbiguous: provider.isAmbiguous ?? false,
|
|
46
|
+
});
|
|
47
|
+
// Stop on unambiguous match
|
|
48
|
+
if (!provider.isAmbiguous) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return matches;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the proof text a user should add to verify a claim
|
|
59
|
+
*/
|
|
60
|
+
export function getProofTextForProvider(providerId: string, did: string, handle?: string): string | undefined {
|
|
61
|
+
const provider = providers[providerId];
|
|
62
|
+
return provider?.getProofText(did, handle);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { github, dns, activitypub, bsky, npm };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ServiceProvider } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slugify a handle for use in npm package names.
|
|
5
|
+
* Replaces dots with dashes since npm doesn't allow dots.
|
|
6
|
+
* e.g. "orta.io" -> "orta-io"
|
|
7
|
+
*/
|
|
8
|
+
function slugifyHandle(handle: string): string {
|
|
9
|
+
return handle.replace(/\./g, "-").toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* npm service provider
|
|
14
|
+
*
|
|
15
|
+
* Users prove ownership by publishing an npm package named keytrace-[handle]
|
|
16
|
+
* containing their DID in the package.json keytrace field.
|
|
17
|
+
*/
|
|
18
|
+
const npm: ServiceProvider = {
|
|
19
|
+
id: "npm",
|
|
20
|
+
name: "npm",
|
|
21
|
+
homepage: "https://www.npmjs.com",
|
|
22
|
+
|
|
23
|
+
// Match npm package URLs: https://www.npmjs.com/package/keytrace-username
|
|
24
|
+
reUri: /^https:\/\/(?:www\.)?npmjs\.com\/package\/(keytrace-[a-z0-9_-]+)\/?$/i,
|
|
25
|
+
|
|
26
|
+
isAmbiguous: false,
|
|
27
|
+
|
|
28
|
+
ui: {
|
|
29
|
+
description: "Link via an npm package",
|
|
30
|
+
icon: "npm",
|
|
31
|
+
inputLabel: "npm Package URL",
|
|
32
|
+
inputPlaceholder: "https://www.npmjs.com/package/keytrace-yourhandle",
|
|
33
|
+
inputDefaultTemplate: "https://www.npmjs.com/package/keytrace-{slugHandle}",
|
|
34
|
+
instructions: [
|
|
35
|
+
"Create a new folder with a `package.json` containing the verification content below",
|
|
36
|
+
"Run `npm publish --access public` to publish",
|
|
37
|
+
"Paste the npm package URL below",
|
|
38
|
+
],
|
|
39
|
+
proofTemplate: `{
|
|
40
|
+
"name": "keytrace-{slugHandle}",
|
|
41
|
+
"version": "0.0.1",
|
|
42
|
+
"keytrace": "{claimId}",
|
|
43
|
+
"did": "{did}"
|
|
44
|
+
}`,
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
processURI(uri, match) {
|
|
48
|
+
const [, packageName] = match;
|
|
49
|
+
// Extract the handle from keytrace-handle
|
|
50
|
+
const handle = packageName.replace(/^keytrace-/i, "");
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
profile: {
|
|
54
|
+
display: `~${handle}`,
|
|
55
|
+
uri: `https://www.npmjs.com/~${handle}`,
|
|
56
|
+
},
|
|
57
|
+
proof: {
|
|
58
|
+
request: {
|
|
59
|
+
// Use npm registry to get the packument with full metadata
|
|
60
|
+
uri: `https://registry.npmjs.org/${packageName}`,
|
|
61
|
+
fetcher: "http",
|
|
62
|
+
format: "json",
|
|
63
|
+
},
|
|
64
|
+
target: [
|
|
65
|
+
// Check for DID in the did field of any version
|
|
66
|
+
// The packument has versions as an object: { versions: { "0.0.1": { did: "did:...", keytrace: "..." } } }
|
|
67
|
+
{
|
|
68
|
+
path: ["versions", "*", "did"],
|
|
69
|
+
relation: "contains",
|
|
70
|
+
format: "text",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
postprocess(data, _match) {
|
|
78
|
+
const packument = data as {
|
|
79
|
+
maintainers?: Array<{ name: string; email?: string }>;
|
|
80
|
+
author?: { name?: string; email?: string } | string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Get the first maintainer's npm username
|
|
84
|
+
const maintainer = packument.maintainers?.[0];
|
|
85
|
+
const npmUsername = maintainer?.name;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
subject: npmUsername,
|
|
89
|
+
profileUrl: npmUsername ? `https://www.npmjs.com/~${npmUsername}` : undefined,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
getProofText(did, handle) {
|
|
94
|
+
const slug = handle ? slugifyHandle(handle) : "[your-handle]";
|
|
95
|
+
return `{
|
|
96
|
+
"name": "keytrace-${slug}",
|
|
97
|
+
"version": "0.0.1",
|
|
98
|
+
"keytrace": "[claim-id]",
|
|
99
|
+
"did": "${did}"
|
|
100
|
+
}`;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
getProofLocation() {
|
|
104
|
+
return `Publish an npm package named keytrace-[your-handle] with your DID in the keytrace field of package.json`;
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
tests: [
|
|
108
|
+
{ uri: "https://www.npmjs.com/package/keytrace-alice", shouldMatch: true },
|
|
109
|
+
{ uri: "https://npmjs.com/package/keytrace-alice", shouldMatch: true },
|
|
110
|
+
{ uri: "https://www.npmjs.com/package/keytrace-alice/", shouldMatch: true },
|
|
111
|
+
{ uri: "https://www.npmjs.com/package/some-other-pkg", shouldMatch: false },
|
|
112
|
+
{ uri: "https://www.npmjs.com/~alice", shouldMatch: false },
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default npm;
|