Skip to content

Commit 1575853

Browse files
authored
Merge pull request #2 from auths-dev/dev-updateTests
tests: update tests to new API
2 parents 357a460 + 2337bc9 commit 1575853

2 files changed

Lines changed: 125 additions & 174 deletions

File tree

src/resolvers/github.ts

Lines changed: 61 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
/**
2-
* GitHub adapter — resolves auths identity data via GitHub REST API.
2+
* GitHub adapter — resolves auths attestations from GitHub Releases.
33
*
4-
* Reads from refs/auths/registry — structured tree with:
5-
* v1/identities/XX/YY/<prefix>/state.json (KERI identity state)
6-
* v1/devices/XX/YY/<did>/attestation.json (device attestations)
4+
* Workflow:
5+
* 1. Fetch latest release via GitHub API
6+
* 2. Find the *.auths.json release asset
7+
* 3. Use device_public_key from the attestation as the verification key
8+
*
9+
* The attestation file is self-contained for device-only attestations
10+
* (no identity_signature): device_signature is verified against
11+
* device_public_key, both of which are in the file.
712
*/
813

914
import type { ForgeAdapter } from './adapter';
1015
import type { ForgeConfig, RefEntry, ResolveResult } from './types';
11-
import { cesrToPublicKeyHex } from './did-utils';
12-
13-
const REGISTRY_REF = 'refs/auths/registry';
1416

15-
async function githubFetch(url: string): Promise<Response> {
17+
async function githubFetch(url: string, accept?: string): Promise<Response> {
1618
const res = await fetch(url, {
17-
headers: { Accept: 'application/vnd.github.v3+json' },
19+
headers: { Accept: accept || 'application/vnd.github.v3+json' },
1820
});
1921
if (!res.ok) {
2022
throw new Error(`GitHub API ${res.status}: ${res.statusText} (${url})`);
@@ -23,103 +25,78 @@ async function githubFetch(url: string): Promise<Response> {
2325
}
2426

2527
export const githubAdapter: ForgeAdapter = {
26-
async listAuthsRefs(config: ForgeConfig): Promise<RefEntry[]> {
27-
const url = `${config.baseUrl}/repos/${config.owner}/${config.repo}/git/matching-refs/auths/`;
28-
const res = await githubFetch(url);
29-
const data: Array<{ ref: string; object: { sha: string } }> = await res.json();
30-
return data.map((entry) => ({ ref: entry.ref, sha: entry.object.sha }));
28+
/**
29+
* Not used in release-asset-based resolution.
30+
* GitHub adapter resolves from /releases/latest, not from Git refs.
31+
* Kept as stub for ForgeAdapter interface compatibility (Gitea uses these).
32+
*/
33+
async listAuthsRefs(_config: ForgeConfig): Promise<RefEntry[]> {
34+
return [];
3135
},
3236

33-
async readBlob(config: ForgeConfig, sha: string): Promise<string> {
34-
const url = `${config.baseUrl}/repos/${config.owner}/${config.repo}/git/blobs/${sha}`;
35-
const res = await githubFetch(url);
36-
const data: { content: string; encoding: string } = await res.json();
37-
if (data.encoding === 'base64') {
38-
return atob(data.content.replace(/\n/g, ''));
39-
}
40-
return data.content;
37+
/** @see listAuthsRefs — same rationale */
38+
async readBlob(_config: ForgeConfig, _sha: string): Promise<string> {
39+
return '';
4140
},
4241

4342
async resolve(config: ForgeConfig, identityFilter?: string): Promise<ResolveResult> {
4443
try {
45-
const refs = await this.listAuthsRefs(config);
46-
if (refs.length === 0) {
47-
return { bundle: null, error: 'No auths refs found in this repository' };
44+
// Fetch latest release
45+
const releaseUrl = `${config.baseUrl}/repos/${config.owner}/${config.repo}/releases/latest`;
46+
const releaseRes = await githubFetch(releaseUrl);
47+
const release: { assets: Array<{ id: number; name: string; browser_download_url: string }> } =
48+
await releaseRes.json();
49+
50+
if (!release.assets || release.assets.length === 0) {
51+
return { bundle: null, error: 'No assets found in latest release' };
4852
}
4953

50-
const registryRef = refs.find((r) => r.ref === REGISTRY_REF);
51-
if (!registryRef) {
52-
return { bundle: null, error: 'No registry ref found (refs/auths/registry)' };
54+
// Find *.auths.json asset
55+
const attestationAsset = release.assets.find((a) => a.name.endsWith('.auths.json'));
56+
if (!attestationAsset) {
57+
return { bundle: null, error: 'No .auths.json attestation found in latest release' };
5358
}
5459

55-
// Get commit → tree SHA
56-
const commitUrl = `${config.baseUrl}/repos/${config.owner}/${config.repo}/git/commits/${registryRef.sha}`;
57-
const commitRes = await githubFetch(commitUrl);
58-
const commit: { tree: { sha: string } } = await commitRes.json();
59-
60-
// Get full recursive tree
61-
const treeUrl = `${config.baseUrl}/repos/${config.owner}/${config.repo}/git/trees/${commit.tree.sha}?recursive=1`;
62-
const treeRes = await githubFetch(treeUrl);
63-
const tree: { tree: Array<{ path: string; sha: string; type: string }> } = await treeRes.json();
64-
65-
// Find identity state.json
66-
const stateEntry = tree.tree.find(
67-
(e) => e.type === 'blob' && /^v1\/identities\/[^/]{2}\/[^/]{2}\/[^/]+\/state\.json$/.test(e.path),
68-
);
69-
if (!stateEntry) {
70-
return { bundle: null, error: 'No identity state found in registry' };
71-
}
72-
73-
// Extract KERI prefix from path: v1/identities/XX/YY/<prefix>/state.json
74-
const keriPrefix = stateEntry.path.split('/')[4];
75-
const controllerDid = `did:keri:${keriPrefix}`;
60+
// Try to download attestation via Contents API (works if file is committed to repo)
61+
// Fall back to asset API endpoint if not found (for repos with assets only)
62+
let attestation: {
63+
issuer: string;
64+
subject: string;
65+
device_public_key: string;
66+
};
7667

77-
if (identityFilter && controllerDid !== identityFilter) {
78-
return {
79-
bundle: null,
80-
error: `Identity ${controllerDid} does not match filter ${identityFilter}`,
81-
};
68+
try {
69+
const contentsUrl = `${config.baseUrl}/repos/${config.owner}/${config.repo}/contents/${attestationAsset.name}`;
70+
const contentsRes = await githubFetch(contentsUrl);
71+
const contentsData: { content: string } = await contentsRes.json();
72+
attestation = JSON.parse(atob(contentsData.content.replace(/\n/g, '')));
73+
} catch {
74+
// Fall back to asset API endpoint if file not in tree
75+
const assetUrl = `${config.baseUrl}/repos/${config.owner}/${config.repo}/releases/assets/${attestationAsset.id}`;
76+
const assetRes = await githubFetch(assetUrl, 'application/octet-stream');
77+
attestation = await assetRes.json();
8278
}
8379

84-
// Read state.json to get current public key (CESR-encoded)
85-
const stateBlob = await this.readBlob(config, stateEntry.sha);
86-
const state = JSON.parse(stateBlob);
87-
const currentKeyCesr: string | undefined = state.state?.current_keys?.[0];
88-
89-
if (!currentKeyCesr) {
90-
return { bundle: null, error: 'No current key found in identity state' };
80+
if (!attestation.issuer || !attestation.device_public_key) {
81+
return { bundle: null, error: 'Attestation missing required fields (issuer, device_public_key)' };
9182
}
9283

93-
let publicKeyHex: string;
94-
try {
95-
publicKeyHex = cesrToPublicKeyHex(currentKeyCesr);
96-
} catch (err) {
84+
if (identityFilter && attestation.issuer !== identityFilter) {
9785
return {
9886
bundle: null,
99-
error: `Failed to decode CESR key: ${err instanceof Error ? err.message : String(err)}`,
87+
error: `Issuer ${attestation.issuer} does not match filter ${identityFilter}`,
10088
};
10189
}
10290

103-
// Find all device attestation.json blobs
104-
const attestationEntries = tree.tree.filter(
105-
(e) => e.type === 'blob' && /^v1\/devices\/[^/]{2}\/[^/]{2}\/[^/]+\/attestation\.json$/.test(e.path),
106-
);
107-
108-
const attestationChain: object[] = [];
109-
for (const entry of attestationEntries) {
110-
try {
111-
const blob = await this.readBlob(config, entry.sha);
112-
attestationChain.push(JSON.parse(blob));
113-
} catch {
114-
// Skip unreadable attestations
115-
}
116-
}
117-
91+
// device_public_key is used as the root key.
92+
// For device-only attestations (no identity_signature), the verifier
93+
// skips the identity check and only verifies device_signature against
94+
// device_public_key — both present in the file.
11895
return {
11996
bundle: {
120-
identity_did: controllerDid,
121-
public_key_hex: publicKeyHex,
122-
attestation_chain: attestationChain,
97+
identity_did: attestation.issuer,
98+
public_key_hex: attestation.device_public_key,
99+
attestation_chain: [attestation],
123100
},
124101
};
125102
} catch (err) {

0 commit comments

Comments
 (0)