Skip to content

Commit 39cf5cb

Browse files
author
Test User
committed
feat: add automated resolver
1 parent 26201e3 commit 39cf5cb

18 files changed

Lines changed: 1175 additions & 7 deletions

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
12+
- **resolver:** Auto-resolve identity and attestation data from a repository URL via new `repo` attribute. No more manual JSON — `<auths-verify repo="https://github.com/user/repo">` just works.
13+
- **resolver:** `forge` attribute to override auto-detection of forge type (`github`, `gitea`, `gitlab`).
14+
- **resolver:** `identity` attribute to filter to a specific DID when a repository has multiple identities.
15+
- **resolver:** GitHub adapter — resolves `refs/auths/identity` and `refs/auths/devices/nodes/*/` via GitHub REST API, extracts public key from `did:key:z...`, reads attestation chain.
16+
- **resolver:** Gitea adapter — mirrors GitHub adapter with `/api/v1/` prefix and configurable base URL for self-hosted instances.
17+
- **resolver:** GitLab stub — returns descriptive error explaining GitLab does not expose custom Git refs via its REST API.
18+
- **resolver:** Pure TypeScript `did:key:z...` to Ed25519 public key hex extraction (inline base58btc decoder, multicodec prefix stripping). Runs before WASM loads.
19+
- **resolver:** URL parser with auto-detection: `github.com` → GitHub, `gitlab.com` → GitLab, unknown hosts → Gitea.
20+
- **resolver:** In-memory cache with 5-minute TTL prevents redundant API calls when multiple widgets point to the same repo.
21+
- **resolver:** Dynamic import (`import('./resolvers/index')`) — zero bundle size impact when `repo` attribute is not used.
22+
- **resolver:** DID sanitization helper matching Rust `layout.rs` (`replace(/[^a-zA-Z0-9]/g, '_')`).
23+
- **tests:** 29 new resolver tests — `detect.test.ts` (10), `did-utils.test.ts` (7), `github.test.ts` (7), `gitea.test.ts` (5).
24+
- **examples:** `auto-resolve.html` demonstrating the `repo` attribute with GitHub, Gitea, forge hints, and identity filters.
25+
26+
### Changed
27+
28+
- **widget:** `#hasInput()` now returns `true` when `repo` is set, even without manual `attestation`/`public-key` data.
29+
- **widget:** `verify()` resolves from forge before loading WASM when `repo` is set but attestation data is missing.
30+
- **README:** Updated quick start to recommend `repo` attribute. Added new attributes to the attribute table.
31+
32+
## [0.1.0] - 2026-02-16
33+
34+
### Added
35+
36+
- Initial release of `<auths-verify>` web component.
37+
- Three display modes: `badge` (default), `detail` (expandable chain table), `tooltip` (hover summary).
38+
- Three badge sizes: `sm`, `md`, `lg`.
39+
- WASM-powered Ed25519 attestation chain verification via `@auths/verifier`.
40+
- Dual build outputs: full bundle (WASM base64-inlined) and slim bundle (separate WASM file).
41+
- Singleton WASM initialization with coalesced loading.
42+
- CSS custom property theming for all states and typography.
43+
- Accessibility: `role="status"`, `aria-live="polite"`, `aria-expanded`, focus-visible outlines, forced-colors support.
44+
- Custom events: `auths-verified`, `auths-error`.
45+
- JavaScript API: `verify()`, `getReport()`.
46+
- Auto-verify on connect and attribute change (debounced).
47+
- SVG icons for all 7 component states.

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,25 @@ A drop-in web component for decentralized commit verification — the decentrali
77

88
## Quick Start
99

10+
### Auto-resolve from a repository (recommended)
11+
1012
```html
1113
<script type="module" src="https://unpkg.com/auths-verify/dist/auths-verify.mjs"></script>
1214

15+
<auths-verify repo="https://github.com/user/repo"></auths-verify>
16+
```
17+
18+
The widget fetches identity and attestation data from the forge's Git refs automatically, loads WASM, and verifies.
19+
20+
### Manual data (advanced)
21+
22+
```html
1323
<auths-verify
1424
attestation='{"version":1,"rid":"...","issuer":"did:keri:...","subject":"did:key:z...","device_public_key":"...","identity_signature":"...","device_signature":"...","revoked":false}'
1525
public-key="aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"
1626
></auths-verify>
1727
```
1828

19-
The widget loads WASM and verifies the attestation automatically on mount.
20-
2129
## Display Modes
2230

2331
### Badge (default)
@@ -48,14 +56,19 @@ Badge with a hover tooltip summarizing verification status.
4856

4957
| Attribute | Type | Default | Description |
5058
|---|---|---|---|
51-
| `attestation` | JSON string | `""` | Single attestation to verify |
52-
| `attestations` | JSON array string | `""` | Chain of attestations to verify |
53-
| `public-key` | hex string | `""` | Root/issuer Ed25519 public key |
59+
| `repo` | URL string | `""` | Repository URL — auto-resolves identity and attestations from forge |
60+
| `forge` | `github\|gitea\|gitlab` | auto | Override forge type auto-detection |
61+
| `identity` | DID string | `""` | Filter to a specific identity DID when repo has multiple |
62+
| `attestation` | JSON string | `""` | Single attestation to verify (manual mode) |
63+
| `attestations` | JSON array string | `""` | Chain of attestations to verify (manual mode) |
64+
| `public-key` | hex string | `""` | Root/issuer Ed25519 public key (manual mode) |
5465
| `mode` | `badge\|detail\|tooltip` | `badge` | Display mode |
5566
| `size` | `sm\|md\|lg` | `md` | Badge size |
5667
| `wasm-url` | string | `""` | Optional WASM URL override |
5768
| `auto-verify` | boolean | `true` | Verify on connect/attribute change |
5869

70+
When `repo` is set, the widget auto-resolves `attestations` and `public-key` from the forge before running WASM verification. GitHub and Gitea are supported. GitLab does not expose custom Git refs — use manual attributes for GitLab repos.
71+
5972
## JavaScript API
6073

6174
```js

examples/auto-resolve.html

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>auths-verify — Auto-Resolve Example</title>
7+
<style>
8+
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 60px auto; padding: 0 20px; }
9+
h1 { font-size: 20px; }
10+
h2 { font-size: 16px; margin-top: 32px; color: #6b7280; }
11+
code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
12+
.demo { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-top: 12px; }
13+
p { font-size: 14px; color: #4b5563; }
14+
pre { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px; overflow-x: auto; font-size: 13px; }
15+
.log { font-size: 12px; color: #6b7280; margin-top: 4px; }
16+
</style>
17+
</head>
18+
<body>
19+
<h1>Auto-Resolve from Repository</h1>
20+
<p>
21+
Instead of supplying raw attestation JSON and a public key hex string,
22+
just point the widget at a repository URL. The widget automatically
23+
fetches identity and attestation data from the forge's Git refs.
24+
</p>
25+
26+
<h2>GitHub Repository</h2>
27+
<p>Uses the <code>repo</code> attribute to auto-resolve from GitHub:</p>
28+
<pre>&lt;auths-verify repo="https://github.com/bordumb/auths"&gt;&lt;/auths-verify&gt;</pre>
29+
<div class="demo">
30+
<auths-verify
31+
id="github-demo"
32+
repo="https://github.com/bordumb/auths"
33+
></auths-verify>
34+
</div>
35+
<p class="log" id="github-log">Waiting...</p>
36+
37+
<h2>With Tooltip Mode</h2>
38+
<p>Combine <code>repo</code> with display modes:</p>
39+
<pre>&lt;auths-verify repo="https://github.com/bordumb/auths" mode="tooltip"&gt;&lt;/auths-verify&gt;</pre>
40+
<div class="demo">
41+
<auths-verify
42+
repo="https://github.com/bordumb/auths"
43+
mode="tooltip"
44+
></auths-verify>
45+
</div>
46+
47+
<h2>Gitea Instance (Self-Hosted)</h2>
48+
<p>For Gitea or other self-hosted forges, auto-detection picks the right adapter:</p>
49+
<pre>&lt;auths-verify repo="https://git.example.com/user/repo"&gt;&lt;/auths-verify&gt;</pre>
50+
<div class="demo">
51+
<auths-verify
52+
repo="https://git.example.com/user/repo"
53+
auto-verify="false"
54+
data-state="idle"
55+
></auths-verify>
56+
<span style="font-size:13px;color:#9ca3af;">(auto-verify off — no live server)</span>
57+
</div>
58+
59+
<h2>Forge Hint Override</h2>
60+
<p>Use <code>forge</code> to override auto-detection:</p>
61+
<pre>&lt;auths-verify repo="https://git.custom.io/org/repo" forge="gitea"&gt;&lt;/auths-verify&gt;</pre>
62+
63+
<h2>Identity Filter</h2>
64+
<p>Use <code>identity</code> to resolve a specific identity when a repo has multiple:</p>
65+
<pre>&lt;auths-verify repo="https://github.com/bordumb/auths" identity="did:key:z6Mk..."&gt;&lt;/auths-verify&gt;</pre>
66+
67+
<script type="module" src="../src/auths-verify.ts"></script>
68+
<script>
69+
const demo = document.getElementById('github-demo');
70+
const log = document.getElementById('github-log');
71+
demo.addEventListener('auths-verified', (e) => {
72+
log.textContent = `Verified! Status: ${e.detail.status.type}, Chain: ${e.detail.chain.length} link(s)`;
73+
});
74+
demo.addEventListener('auths-error', (e) => {
75+
log.textContent = `Error: ${e.detail.error}`;
76+
});
77+
</script>
78+
</body>
79+
</html>

src/auths-verify.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const DEBOUNCE_MS = 50;
1515

1616
class AuthsVerify extends HTMLElement {
1717
static get observedAttributes(): string[] {
18-
return ['attestation', 'attestations', 'public-key', 'mode', 'size', 'wasm-url', 'auto-verify'];
18+
return ['attestation', 'attestations', 'public-key', 'mode', 'size', 'wasm-url', 'auto-verify', 'repo', 'forge', 'identity'];
1919
}
2020

2121
// --- Private state ---
@@ -112,6 +112,27 @@ class AuthsVerify extends HTMLElement {
112112
}
113113
}
114114

115+
get repo(): string {
116+
return this.getAttribute('repo') ?? '';
117+
}
118+
set repo(v: string) {
119+
this.setAttribute('repo', v);
120+
}
121+
122+
get forge(): string {
123+
return this.getAttribute('forge') ?? '';
124+
}
125+
set forge(v: string) {
126+
this.setAttribute('forge', v);
127+
}
128+
129+
get identity(): string {
130+
return this.getAttribute('identity') ?? '';
131+
}
132+
set identity(v: string) {
133+
this.setAttribute('identity', v);
134+
}
135+
115136
// --- Public API ---
116137

117138
/** Trigger verification manually. */
@@ -124,6 +145,21 @@ class AuthsVerify extends HTMLElement {
124145
this.#setState('loading');
125146

126147
try {
148+
// If repo is set but attestation data is missing, resolve from forge
149+
if (this.repo && !this.attestation && !this.attestations) {
150+
const { resolveFromRepo } = await import('./resolvers/index');
151+
const result = await resolveFromRepo(
152+
this.repo,
153+
this.forge || undefined,
154+
this.identity || undefined,
155+
);
156+
if (!result.bundle) {
157+
throw new Error(result.error ?? 'Could not resolve identity from repository');
158+
}
159+
this.publicKey = result.bundle.public_key_hex;
160+
this.attestations = JSON.stringify(result.bundle.attestation_chain);
161+
}
162+
127163
await ensureInit(this.wasmUrl || undefined);
128164

129165
const pk = this.publicKey;
@@ -177,7 +213,7 @@ class AuthsVerify extends HTMLElement {
177213
// --- Internals ---
178214

179215
#hasInput(): boolean {
180-
return !!(this.publicKey && (this.attestation || this.attestations));
216+
return !!(this.repo || (this.publicKey && (this.attestation || this.attestations)));
181217
}
182218

183219
#setState(state: ComponentState): void {

src/resolvers/adapter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ForgeConfig, ResolveResult, RefEntry } from './types';
2+
3+
/** Forge-specific adapter for resolving auths identity data via Git refs */
4+
export interface ForgeAdapter {
5+
resolve(config: ForgeConfig, identityFilter?: string): Promise<ResolveResult>;
6+
listAuthsRefs(config: ForgeConfig): Promise<RefEntry[]>;
7+
readBlob(config: ForgeConfig, sha: string): Promise<string>;
8+
}

src/resolvers/cache.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ResolveResult } from './types';
2+
3+
const TTL_MS = 5 * 60 * 1000; // 5 minutes
4+
5+
interface CacheEntry {
6+
data: ResolveResult;
7+
expires: number;
8+
}
9+
10+
const store = new Map<string, CacheEntry>();
11+
12+
export function cacheGet(key: string): ResolveResult | null {
13+
const entry = store.get(key);
14+
if (!entry) return null;
15+
if (Date.now() > entry.expires) {
16+
store.delete(key);
17+
return null;
18+
}
19+
return entry.data;
20+
}
21+
22+
export function cacheSet(key: string, data: ResolveResult): void {
23+
store.set(key, { data, expires: Date.now() + TTL_MS });
24+
}
25+
26+
export function cacheClear(): void {
27+
store.clear();
28+
}

src/resolvers/detect.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { ForgeConfig, ForgeType } from './types';
2+
3+
/**
4+
* Parse a repository URL and detect the forge type.
5+
*
6+
* - github.com → github
7+
* - gitlab.com → gitlab
8+
* - Unknown host → defaults to gitea (self-hosted)
9+
* - forgeHint overrides auto-detection
10+
*/
11+
export function detectForge(repoUrl: string, forgeHint?: string): ForgeConfig | null {
12+
let url: URL;
13+
try {
14+
url = new URL(repoUrl);
15+
} catch {
16+
return null;
17+
}
18+
19+
// Strip .git suffix and trailing slash from pathname
20+
const path = url.pathname.replace(/\.git$/, '').replace(/\/$/, '');
21+
const segments = path.split('/').filter(Boolean);
22+
23+
if (segments.length < 2) return null;
24+
25+
const owner = segments[0];
26+
const repo = segments[1];
27+
28+
let type: ForgeType;
29+
let baseUrl: string;
30+
31+
if (forgeHint) {
32+
type = forgeHint as ForgeType;
33+
} else {
34+
const host = url.hostname.toLowerCase();
35+
if (host === 'github.com') {
36+
type = 'github';
37+
} else if (host === 'gitlab.com') {
38+
type = 'gitlab';
39+
} else {
40+
type = 'gitea';
41+
}
42+
}
43+
44+
switch (type) {
45+
case 'github':
46+
baseUrl = 'https://api.github.com';
47+
break;
48+
case 'gitlab':
49+
baseUrl = `${url.protocol}//${url.host}`;
50+
break;
51+
case 'gitea':
52+
baseUrl = `${url.protocol}//${url.host}`;
53+
break;
54+
default:
55+
return null;
56+
}
57+
58+
return { type, baseUrl, owner, repo };
59+
}

0 commit comments

Comments
 (0)