@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/dist/types.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity metadata extracted during verification
|
|
3
|
+
*/
|
|
4
|
+
export interface IdentityMetadata {
|
|
5
|
+
/** Display name / username */
|
|
6
|
+
subject?: string;
|
|
7
|
+
/** Avatar/profile image URL */
|
|
8
|
+
avatarUrl?: string;
|
|
9
|
+
/** Profile page URL */
|
|
10
|
+
profileUrl?: string;
|
|
11
|
+
/** Display name if different from subject */
|
|
12
|
+
displayName?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Details about a single proof target check
|
|
16
|
+
*/
|
|
17
|
+
export interface ProofTargetResult {
|
|
18
|
+
path: string[];
|
|
19
|
+
relation: string;
|
|
20
|
+
valuesFound: string[];
|
|
21
|
+
matched: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Details about the proof fetching and matching process
|
|
25
|
+
*/
|
|
26
|
+
export interface ProofDetails {
|
|
27
|
+
/** The URL that was fetched */
|
|
28
|
+
fetchUrl: string;
|
|
29
|
+
/** The fetcher used (http, dns, etc.) */
|
|
30
|
+
fetcher: string;
|
|
31
|
+
/** Raw content that was fetched (truncated if large) */
|
|
32
|
+
content: string;
|
|
33
|
+
/** The proof targets that were checked */
|
|
34
|
+
targets: ProofTargetResult[];
|
|
35
|
+
/** The patterns used for matching (DID variations) */
|
|
36
|
+
patterns: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Result of verifying a claim
|
|
40
|
+
*/
|
|
41
|
+
export interface ClaimVerificationResult {
|
|
42
|
+
status: ClaimStatus;
|
|
43
|
+
errors: string[];
|
|
44
|
+
timestamp: Date;
|
|
45
|
+
/** Identity metadata extracted from the proof source */
|
|
46
|
+
identity?: IdentityMetadata;
|
|
47
|
+
/** Details about the proof fetching and verification process */
|
|
48
|
+
proofDetails?: ProofDetails;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Profile data from ATProto
|
|
52
|
+
*/
|
|
53
|
+
export interface ProfileData {
|
|
54
|
+
did: string;
|
|
55
|
+
handle: string;
|
|
56
|
+
displayName?: string;
|
|
57
|
+
avatar?: string;
|
|
58
|
+
claims: ClaimData[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Individual claim data from ATProto record
|
|
62
|
+
*/
|
|
63
|
+
export interface ClaimData {
|
|
64
|
+
uri: string;
|
|
65
|
+
did: string;
|
|
66
|
+
type?: string;
|
|
67
|
+
comment?: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
rkey: string;
|
|
70
|
+
identity?: IdentityMetadata;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Options for verification operations
|
|
74
|
+
*/
|
|
75
|
+
export interface VerifyOptions {
|
|
76
|
+
/** Timeout for fetcher operations in ms */
|
|
77
|
+
timeout?: number;
|
|
78
|
+
/** Skip cache and force fresh verification */
|
|
79
|
+
skipCache?: boolean;
|
|
80
|
+
/** Proxy URL for browser-based DNS/HTTP requests */
|
|
81
|
+
proxyUrl?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Claim status enum
|
|
85
|
+
*/
|
|
86
|
+
export declare enum ClaimStatus {
|
|
87
|
+
INIT = "init",
|
|
88
|
+
MATCHED = "matched",
|
|
89
|
+
VERIFIED = "verified",
|
|
90
|
+
FAILED = "failed",
|
|
91
|
+
ERROR = "error"
|
|
92
|
+
}
|
|
93
|
+
/** Injected fetch function - allows caller to provide proxy, auth, etc. */
|
|
94
|
+
export type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
|
|
95
|
+
/** Configuration for the recipe runner */
|
|
96
|
+
export interface RunnerConfig {
|
|
97
|
+
/** Custom fetch function (defaults to global fetch) */
|
|
98
|
+
fetch?: FetchFn;
|
|
99
|
+
/** Request timeout in ms (default: 10000) */
|
|
100
|
+
timeout?: number;
|
|
101
|
+
}
|
|
102
|
+
/** Context for a claim verification attempt */
|
|
103
|
+
export interface ClaimContext {
|
|
104
|
+
/** Unique claim ID for this verification attempt */
|
|
105
|
+
claimId: string;
|
|
106
|
+
/** User's ATProto DID */
|
|
107
|
+
did: string;
|
|
108
|
+
/** User's ATProto handle */
|
|
109
|
+
handle: string;
|
|
110
|
+
/** User-provided params from recipe (e.g., { gistUrl: "..." }) */
|
|
111
|
+
params: Record<string, string>;
|
|
112
|
+
}
|
|
113
|
+
/** Result of running a full recipe verification */
|
|
114
|
+
export interface VerificationResult {
|
|
115
|
+
success: boolean;
|
|
116
|
+
steps: StepResult[];
|
|
117
|
+
/** Extracted subject from params (e.g., "github:octocat") */
|
|
118
|
+
subject?: string;
|
|
119
|
+
error?: string;
|
|
120
|
+
}
|
|
121
|
+
/** Result of a single verification step */
|
|
122
|
+
export interface StepResult {
|
|
123
|
+
action: string;
|
|
124
|
+
success: boolean;
|
|
125
|
+
data?: unknown;
|
|
126
|
+
error?: string;
|
|
127
|
+
}
|
|
128
|
+
/** A recipe parameter definition */
|
|
129
|
+
export interface RecipeParam {
|
|
130
|
+
key: string;
|
|
131
|
+
label: string;
|
|
132
|
+
type: "url" | "text" | "domain";
|
|
133
|
+
placeholder?: string;
|
|
134
|
+
pattern?: string;
|
|
135
|
+
extractFrom?: string;
|
|
136
|
+
}
|
|
137
|
+
/** User-facing instructions for how to set up a claim */
|
|
138
|
+
export interface RecipeInstructions {
|
|
139
|
+
steps: string[];
|
|
140
|
+
proofTemplate?: string;
|
|
141
|
+
proofLocation?: string;
|
|
142
|
+
}
|
|
143
|
+
/** A single verification step in a recipe */
|
|
144
|
+
export interface VerificationStep {
|
|
145
|
+
action: "http-get" | "json-path" | "css-select" | "regex-match" | "dns-txt";
|
|
146
|
+
url?: string;
|
|
147
|
+
selector?: string;
|
|
148
|
+
pattern?: string;
|
|
149
|
+
expect?: string;
|
|
150
|
+
}
|
|
151
|
+
/** Machine-readable verification definition */
|
|
152
|
+
export interface RecipeVerification {
|
|
153
|
+
steps: VerificationStep[];
|
|
154
|
+
}
|
|
155
|
+
/** A claim verification recipe */
|
|
156
|
+
export interface Recipe {
|
|
157
|
+
$type?: string;
|
|
158
|
+
type: string;
|
|
159
|
+
version: number;
|
|
160
|
+
displayName: string;
|
|
161
|
+
params?: RecipeParam[];
|
|
162
|
+
instructions: RecipeInstructions;
|
|
163
|
+
verification: RecipeVerification;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim status enum
|
|
3
|
+
*/
|
|
4
|
+
export var ClaimStatus;
|
|
5
|
+
(function (ClaimStatus) {
|
|
6
|
+
ClaimStatus["INIT"] = "init";
|
|
7
|
+
ClaimStatus["MATCHED"] = "matched";
|
|
8
|
+
ClaimStatus["VERIFIED"] = "verified";
|
|
9
|
+
ClaimStatus["FAILED"] = "failed";
|
|
10
|
+
ClaimStatus["ERROR"] = "error";
|
|
11
|
+
})(ClaimStatus || (ClaimStatus = {}));
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAyFA;;GAEG;AACH,MAAM,CAAN,IAAY,WAMX;AAND,WAAY,WAAW;IACrB,4BAAa,CAAA;IACb,kCAAmB,CAAA;IACnB,oCAAqB,CAAA;IACrB,gCAAiB,CAAA;IACjB,8BAAe,CAAA;AACjB,CAAC,EANW,WAAW,KAAX,WAAW,QAMtB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@keytrace/runner",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"files": [
|
|
5
|
+
"src",
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./src/index.ts",
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"lint": "echo 'lint not configured'"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@atproto/api": "^0.14.0",
|
|
26
|
+
"cheerio": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"oxfmt": "^0.27.0",
|
|
31
|
+
"typescript": "^5.7.0",
|
|
32
|
+
"vitest": "^2.1.0"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse HTML and extract text content using a CSS selector.
|
|
5
|
+
* Uses cheerio for server-side HTML parsing.
|
|
6
|
+
*/
|
|
7
|
+
export function cssSelect(html: string, selector: string): string {
|
|
8
|
+
const $ = cheerio.load(html);
|
|
9
|
+
const elements = $(selector);
|
|
10
|
+
if (elements.length === 0) {
|
|
11
|
+
throw new Error(`No elements matched selector: "${selector}"`);
|
|
12
|
+
}
|
|
13
|
+
return elements.first().text().trim();
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perform a DNS TXT record lookup for a domain.
|
|
3
|
+
* Node only - throws a descriptive error in browser environments.
|
|
4
|
+
*/
|
|
5
|
+
export async function dnsTxt(domain: string): Promise<string[]> {
|
|
6
|
+
let dns: typeof import("node:dns/promises");
|
|
7
|
+
try {
|
|
8
|
+
dns = await import("node:dns/promises");
|
|
9
|
+
} catch {
|
|
10
|
+
throw new Error("DNS TXT lookups are not available in the browser. " + "Use the server-side proxy endpoint (POST /api/proxy/dns) instead.");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const records = await dns.resolveTxt(domain);
|
|
14
|
+
// resolveTxt returns string[][] - flatten to string[]
|
|
15
|
+
return records.map((chunks) => chunks.join(""));
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FetchFn } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch a URL using the injected fetch function and return the response body as text.
|
|
5
|
+
*/
|
|
6
|
+
export async function httpGet(url: string, fetchFn: FetchFn, timeout?: number): Promise<string> {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timeoutId = timeout ? setTimeout(() => controller.abort(), timeout) : undefined;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetchFn(url, { signal: controller.signal });
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
14
|
+
}
|
|
15
|
+
return await response.text();
|
|
16
|
+
} finally {
|
|
17
|
+
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract data from a JSON string (or parsed object) using a simple dot-notation path.
|
|
3
|
+
*
|
|
4
|
+
* Supports paths like:
|
|
5
|
+
* "$.keytrace" -> obj.keytrace
|
|
6
|
+
* "$.data.name" -> obj.data.name
|
|
7
|
+
* "$.items[0].id" -> obj.items[0].id
|
|
8
|
+
*
|
|
9
|
+
* The leading "$." is optional.
|
|
10
|
+
*/
|
|
11
|
+
export function jsonPath(data: string | object, selector: string): unknown {
|
|
12
|
+
const obj = typeof data === "string" ? JSON.parse(data) : data;
|
|
13
|
+
|
|
14
|
+
// Strip leading $. if present
|
|
15
|
+
const path = selector.startsWith("$.") ? selector.slice(2) : selector;
|
|
16
|
+
|
|
17
|
+
// Split on dots and bracket notation
|
|
18
|
+
const segments = path.split(/\.|\[(\d+)\]/).filter((s) => s !== "" && s !== undefined);
|
|
19
|
+
|
|
20
|
+
let current: unknown = obj;
|
|
21
|
+
for (const segment of segments) {
|
|
22
|
+
if (current == null || typeof current !== "object") {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
current = (current as Record<string, unknown>)[segment];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match content against a regex pattern.
|
|
3
|
+
* Returns the first capture group if present, otherwise the full match.
|
|
4
|
+
*/
|
|
5
|
+
export function regexMatch(content: string, pattern: string): string {
|
|
6
|
+
const regex = new RegExp(pattern);
|
|
7
|
+
const match = content.match(regex);
|
|
8
|
+
if (!match) {
|
|
9
|
+
throw new Error(`Pattern "${pattern}" did not match content`);
|
|
10
|
+
}
|
|
11
|
+
// Return first capture group if available, otherwise the full match
|
|
12
|
+
return match[1] ?? match[0];
|
|
13
|
+
}
|
package/src/claim.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { ClaimStatus } from "./types.js";
|
|
2
|
+
import { DEFAULT_TIMEOUT } from "./constants.js";
|
|
3
|
+
import { matchUri, type ServiceProviderMatch, type ProofRequest, type ProofTarget } from "./serviceProviders/index.js";
|
|
4
|
+
import * as fetchers from "./fetchers/index.js";
|
|
5
|
+
import type { VerifyOptions, ClaimVerificationResult, IdentityMetadata, ProofDetails, ProofTargetResult } from "./types.js";
|
|
6
|
+
|
|
7
|
+
// did:plc identifiers are base32-encoded, lowercase
|
|
8
|
+
const DID_PLC_RE = /^did:plc:[a-z2-7]{24}$/;
|
|
9
|
+
// did:web uses a domain name (with optional port and path segments encoded as colons)
|
|
10
|
+
const DID_WEB_RE = /^did:web:[a-zA-Z0-9._:%-]+$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a DID string. Accepts did:plc and did:web formats.
|
|
14
|
+
*/
|
|
15
|
+
export function isValidDid(did: string): boolean {
|
|
16
|
+
return DID_PLC_RE.test(did) || DID_WEB_RE.test(did);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A single identity claim linking a DID to an external account
|
|
21
|
+
*/
|
|
22
|
+
export interface ClaimState {
|
|
23
|
+
uri: string;
|
|
24
|
+
did: string;
|
|
25
|
+
status: ClaimStatus;
|
|
26
|
+
matches: ServiceProviderMatch[];
|
|
27
|
+
errors: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a new claim state
|
|
32
|
+
*/
|
|
33
|
+
export function createClaim(uri: string, did: string): ClaimState {
|
|
34
|
+
if (!isValidDid(did)) {
|
|
35
|
+
throw new Error(`Invalid DID format: ${did}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
uri,
|
|
39
|
+
did,
|
|
40
|
+
status: ClaimStatus.INIT,
|
|
41
|
+
matches: [],
|
|
42
|
+
errors: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Match the claim URI against known service providers
|
|
48
|
+
*/
|
|
49
|
+
export function matchClaim(claim: ClaimState): void {
|
|
50
|
+
claim.matches = matchUri(claim.uri);
|
|
51
|
+
claim.status = claim.matches.length > 0 ? ClaimStatus.MATCHED : ClaimStatus.ERROR;
|
|
52
|
+
|
|
53
|
+
if (claim.matches.length === 0) {
|
|
54
|
+
claim.errors.push(`No service provider matched URI: ${claim.uri}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if the claim is ambiguous (matches multiple providers)
|
|
60
|
+
*/
|
|
61
|
+
export function isClaimAmbiguous(claim: ClaimState): boolean {
|
|
62
|
+
return claim.matches.length > 1 || (claim.matches.length === 1 && claim.matches[0].isAmbiguous);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the matched service provider (first unambiguous match, or first match)
|
|
67
|
+
*/
|
|
68
|
+
export function getMatchedProvider(claim: ClaimState): ServiceProviderMatch | undefined {
|
|
69
|
+
return claim.matches[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Verify the claim by fetching proof and checking for DID
|
|
74
|
+
*/
|
|
75
|
+
export async function verifyClaim(claim: ClaimState, opts: VerifyOptions = {}): Promise<ClaimVerificationResult> {
|
|
76
|
+
if (claim.status === ClaimStatus.INIT) {
|
|
77
|
+
matchClaim(claim);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (claim.matches.length === 0) {
|
|
81
|
+
return {
|
|
82
|
+
status: ClaimStatus.ERROR,
|
|
83
|
+
errors: claim.errors,
|
|
84
|
+
timestamp: new Date(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Track proof details for the response
|
|
89
|
+
let proofDetails: ProofDetails | undefined;
|
|
90
|
+
|
|
91
|
+
// Try each matched provider until one succeeds
|
|
92
|
+
for (const match of claim.matches) {
|
|
93
|
+
try {
|
|
94
|
+
const config = match.provider.processURI(claim.uri, match.match);
|
|
95
|
+
const proofData = await fetchProof(config.proof.request, opts);
|
|
96
|
+
|
|
97
|
+
// Build proof details
|
|
98
|
+
const patterns = generateProofPatterns(claim.did);
|
|
99
|
+
const targetResults = checkProofWithDetails(proofData, config.proof.target, patterns);
|
|
100
|
+
|
|
101
|
+
proofDetails = {
|
|
102
|
+
fetchUrl: config.proof.request.uri,
|
|
103
|
+
fetcher: config.proof.request.fetcher,
|
|
104
|
+
content: truncateContent(proofData),
|
|
105
|
+
targets: targetResults,
|
|
106
|
+
patterns,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const verified = targetResults.some((t) => t.matched);
|
|
110
|
+
|
|
111
|
+
if (verified) {
|
|
112
|
+
claim.status = ClaimStatus.VERIFIED;
|
|
113
|
+
|
|
114
|
+
// Extract identity metadata via postprocess if available
|
|
115
|
+
let identity: IdentityMetadata | undefined;
|
|
116
|
+
if (match.provider.postprocess) {
|
|
117
|
+
const metadata = match.provider.postprocess(proofData, match.match);
|
|
118
|
+
identity = {
|
|
119
|
+
subject: metadata.subject,
|
|
120
|
+
avatarUrl: metadata.avatarUrl,
|
|
121
|
+
profileUrl: metadata.profileUrl,
|
|
122
|
+
displayName: metadata.displayName,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
status: ClaimStatus.VERIFIED,
|
|
128
|
+
errors: [],
|
|
129
|
+
timestamp: new Date(),
|
|
130
|
+
identity,
|
|
131
|
+
proofDetails,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
claim.errors.push(`${match.provider.id}: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Stop on unambiguous match
|
|
139
|
+
if (!match.isAmbiguous) break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
claim.status = ClaimStatus.FAILED;
|
|
143
|
+
return {
|
|
144
|
+
status: ClaimStatus.FAILED,
|
|
145
|
+
errors: claim.errors,
|
|
146
|
+
timestamp: new Date(),
|
|
147
|
+
proofDetails,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function fetchProof(request: ProofRequest, opts: VerifyOptions): Promise<unknown> {
|
|
152
|
+
const fetcher = fetchers.get(request.fetcher);
|
|
153
|
+
if (!fetcher) {
|
|
154
|
+
throw new Error(`Unknown fetcher: ${request.fetcher}`);
|
|
155
|
+
}
|
|
156
|
+
console.log(`[runner] Fetching proof: ${request.fetcher} ${request.uri} (format: ${request.format})`);
|
|
157
|
+
const data = await fetcher.fetch(request.uri, {
|
|
158
|
+
format: request.format,
|
|
159
|
+
timeout: opts.timeout ?? DEFAULT_TIMEOUT,
|
|
160
|
+
headers: request.options?.headers,
|
|
161
|
+
});
|
|
162
|
+
const fileKeys = data && typeof data === "object" && "files" in data ? Object.keys((data as Record<string, unknown>).files as object) : [];
|
|
163
|
+
console.log(`[runner] Fetched proof, files: ${JSON.stringify(fileKeys)}`);
|
|
164
|
+
return data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function checkProofWithDetails(data: unknown, targets: ProofTarget[], patterns: string[]): ProofTargetResult[] {
|
|
168
|
+
console.log(`[runner] Checking proof, patterns: ${JSON.stringify(patterns)}`);
|
|
169
|
+
console.log(`[runner] Proof targets: ${JSON.stringify(targets.map((t) => t.path.join(".")))}`);
|
|
170
|
+
|
|
171
|
+
const results: ProofTargetResult[] = [];
|
|
172
|
+
|
|
173
|
+
for (const target of targets) {
|
|
174
|
+
const values = extractValues(data, target.path);
|
|
175
|
+
console.log(`[runner] Target ${target.path.join(".")}: found ${values.length} value(s)${values.length > 0 ? `: ${JSON.stringify(values.map((v) => v.slice(0, 100)))}` : ""}`);
|
|
176
|
+
|
|
177
|
+
let matched = false;
|
|
178
|
+
for (const value of values) {
|
|
179
|
+
if (matchesPattern(value, patterns, target.relation)) {
|
|
180
|
+
console.log(`[runner] Match found at ${target.path.join(".")} (relation: ${target.relation})`);
|
|
181
|
+
matched = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
results.push({
|
|
187
|
+
path: target.path,
|
|
188
|
+
relation: target.relation,
|
|
189
|
+
valuesFound: values.map((v) => v.slice(0, 500)), // Truncate long values
|
|
190
|
+
matched,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!results.some((r) => r.matched)) {
|
|
195
|
+
console.log(`[runner] No match found in any target`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return results;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function truncateContent(data: unknown): string {
|
|
202
|
+
const MAX_LENGTH = 2000;
|
|
203
|
+
let content: string;
|
|
204
|
+
|
|
205
|
+
if (data === null || data === undefined) {
|
|
206
|
+
return "(no content returned)";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof data === "string") {
|
|
210
|
+
content = data;
|
|
211
|
+
} else {
|
|
212
|
+
try {
|
|
213
|
+
content = JSON.stringify(data, null, 2);
|
|
214
|
+
} catch {
|
|
215
|
+
content = String(data);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (content.length > MAX_LENGTH) {
|
|
220
|
+
return content.slice(0, MAX_LENGTH) + "\n... (truncated)";
|
|
221
|
+
}
|
|
222
|
+
return content;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function generateProofPatterns(did: string): string[] {
|
|
226
|
+
const patterns = [did];
|
|
227
|
+
|
|
228
|
+
if (did.startsWith("did:plc:")) {
|
|
229
|
+
patterns.push(did.replace("did:plc:", ""));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return patterns;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractValues(data: unknown, path: string[]): string[] {
|
|
236
|
+
const results: string[] = [];
|
|
237
|
+
extractValuesRecursive(data, path, 0, results);
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function extractValuesRecursive(data: unknown, path: string[], index: number, results: string[]): void {
|
|
242
|
+
if (data === null || data === undefined) return;
|
|
243
|
+
|
|
244
|
+
if (index >= path.length) {
|
|
245
|
+
if (typeof data === "string") {
|
|
246
|
+
results.push(data);
|
|
247
|
+
} else if (Array.isArray(data)) {
|
|
248
|
+
for (const item of data) {
|
|
249
|
+
if (typeof item === "string") {
|
|
250
|
+
results.push(item);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const key = path[index];
|
|
258
|
+
|
|
259
|
+
if (key === "*") {
|
|
260
|
+
if (Array.isArray(data)) {
|
|
261
|
+
// Wildcard for arrays: iterate all items
|
|
262
|
+
for (const item of data) {
|
|
263
|
+
extractValuesRecursive(item, path, index + 1, results);
|
|
264
|
+
}
|
|
265
|
+
} else if (typeof data === "object" && data !== null) {
|
|
266
|
+
// Wildcard for objects: iterate all values
|
|
267
|
+
const record = data as Record<string, unknown>;
|
|
268
|
+
for (const value of Object.values(record)) {
|
|
269
|
+
extractValuesRecursive(value, path, index + 1, results);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else if (typeof data === "object" && data !== null) {
|
|
273
|
+
const record = data as Record<string, unknown>;
|
|
274
|
+
extractValuesRecursive(record[key], path, index + 1, results);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function matchesPattern(value: string, patterns: string[], relation: "contains" | "equals" | "startsWith"): boolean {
|
|
279
|
+
for (const pattern of patterns) {
|
|
280
|
+
switch (relation) {
|
|
281
|
+
case "contains":
|
|
282
|
+
if (value.includes(pattern)) return true;
|
|
283
|
+
break;
|
|
284
|
+
case "equals":
|
|
285
|
+
if (value === pattern) return true;
|
|
286
|
+
break;
|
|
287
|
+
case "startsWith":
|
|
288
|
+
if (value.startsWith(pattern)) return true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ATProto collection NSID for identity claims
|
|
3
|
+
*/
|
|
4
|
+
export const COLLECTION_NSID = "dev.keytrace.claim";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default timeout for fetcher operations in milliseconds
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_TIMEOUT = 5000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PLC directory URL for resolving did:plc DIDs
|
|
13
|
+
*/
|
|
14
|
+
export const PLC_DIRECTORY_URL = "https://plc.directory";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fallback public ATProto API URL (used only when PDS resolution fails)
|
|
18
|
+
*/
|
|
19
|
+
export const PUBLIC_API_URL = "https://public.api.bsky.app";
|
package/src/expect.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse an expect string and compare against a value.
|
|
3
|
+
*
|
|
4
|
+
* Supported formats:
|
|
5
|
+
* "equals:{value}" - exact string match
|
|
6
|
+
* "contains:{value}" - substring match
|
|
7
|
+
*/
|
|
8
|
+
export function checkExpect(expectStr: string, actual: unknown): { pass: boolean; message: string } {
|
|
9
|
+
const colonIdx = expectStr.indexOf(":");
|
|
10
|
+
if (colonIdx === -1) {
|
|
11
|
+
return {
|
|
12
|
+
pass: false,
|
|
13
|
+
message: `Invalid expect format: "${expectStr}" (expected "type:value")`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const type = expectStr.slice(0, colonIdx);
|
|
18
|
+
const expected = expectStr.slice(colonIdx + 1);
|
|
19
|
+
const actualStr = String(actual ?? "");
|
|
20
|
+
|
|
21
|
+
switch (type) {
|
|
22
|
+
case "equals":
|
|
23
|
+
return actualStr === expected ? { pass: true, message: `Value equals "${expected}"` } : { pass: false, message: `Expected "${expected}" but got "${actualStr}"` };
|
|
24
|
+
|
|
25
|
+
case "contains":
|
|
26
|
+
return actualStr.includes(expected)
|
|
27
|
+
? { pass: true, message: `Value contains "${expected}"` }
|
|
28
|
+
: {
|
|
29
|
+
pass: false,
|
|
30
|
+
message: `Expected value to contain "${expected}" but got "${actualStr}"`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
default:
|
|
34
|
+
return { pass: false, message: `Unknown expect type: "${type}"` };
|
|
35
|
+
}
|
|
36
|
+
}
|