Skip to content

Commit d51670d

Browse files
committed
test: extract and execute signing-example code blocks from docs
Defends against docs/test divergence by making the docs themselves the test source. The new test reads README.md and the building-with-hypercerts-lexicons skill, finds the "Cryptographic Signatures" section in each, extracts every fenced TypeScript code block under it, concatenates them into one ESM module (deduping imports and appending synthetic exports for the identifiers we want to inspect), writes the result to tests/extracted/ (gitignored), and dynamically imports. For each source file, asserts: - Extraction + execution succeeds without throwing - The produced CID is a 36-byte CIDv1 with the dag-cbor/SHA-256 prefix - Secp256k1Keypair.sign() returns 64 raw r,s bytes - The inline signature has the correct \$type and references the signature bytes - The remote-attestation strongRef has the correct \$type and at:// URI - Both signatures attach to the final record in order - The resulting record (with a syntactically valid CID substituted for the "bafy..." placeholder) passes lexicon validation - The produced signature verifies via @atproto/crypto verifySignature Adds @atproto/crypto and @ipld/dag-cbor to devDependencies (only used by this test; not exposed to package consumers).
1 parent 3627bc7 commit d51670d

4 files changed

Lines changed: 314 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dist/
44
generated/
55
pnpm-lock.yaml
66
/tmp/
7+
tests/extracted/
78
lexicons.md
89

910
# Dolt database files (added by bd init)

package-lock.json

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@
6969
"multiformats": "^13.3.6"
7070
},
7171
"devDependencies": {
72+
"@atproto/crypto": "^0.5.0",
7273
"@atproto/lex-cli": "^0.9.5",
7374
"@atproto/xrpc": "^0.7.5",
7475
"@changesets/changelog-github": "^0.5.2",
7576
"@changesets/cli": "^2.29.8",
7677
"@eslint/js": "^9.39.2",
78+
"@ipld/dag-cbor": "^10.0.1",
7779
"@rollup/plugin-commonjs": "^29.0.0",
7880
"@rollup/plugin-json": "^6.1.0",
7981
"@rollup/plugin-node-resolve": "^16.0.3",
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/**
2+
* End-to-end execution of the cryptographic-signature example in
3+
* README.md and the building-with-hypercerts-lexicons skill.
4+
*
5+
* The docs ARE the test source. Each markdown file's "Cryptographic
6+
* Signatures" section contains a sequence of TypeScript code blocks
7+
* that together build an inline signature, a remote-attestation
8+
* strongRef, and the final signed record. This test extracts those
9+
* blocks, concatenates them into a single ESM module, dynamically
10+
* imports it, and verifies:
11+
*
12+
* 1. The example runs end-to-end without throwing
13+
* 2. The produced inlineSignature has the correct shape and bytes
14+
* 3. The produced signedActivity is lexicon-valid
15+
* 4. The produced signature actually verifies via @atproto/crypto
16+
*
17+
* If the docs example drifts (renamed API, broken procedure, removed
18+
* lexicon ref, etc.), this test fails. If the test logic needs to
19+
* change, the docs are what to edit — not the test fixture.
20+
*/
21+
import { describe, it, expect } from "vitest";
22+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
23+
import { resolve, dirname } from "path";
24+
import { fileURLToPath, pathToFileURL } from "url";
25+
import { verifySignature } from "@atproto/crypto";
26+
import * as Activity from "../generated/types/org/hypercerts/claim/activity.js";
27+
28+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
29+
const EXTRACT_DIR = resolve(ROOT, "tests/extracted");
30+
31+
interface ExtractedExample {
32+
signedActivity: { signatures: unknown[] };
33+
inlineSignature: {
34+
$type: string;
35+
signature: Uint8Array;
36+
key: string;
37+
};
38+
remoteAttestation: {
39+
$type: string;
40+
uri: string;
41+
cid: string;
42+
};
43+
cidBytes: Uint8Array;
44+
signatureBytes: Uint8Array;
45+
keypair: { did(): string };
46+
}
47+
48+
/**
49+
* Extract all TypeScript code blocks under a heading whose text
50+
* matches `sectionPattern` (and any nested subheadings until the next
51+
* heading at the same or shallower level).
52+
*/
53+
function extractSectionBlocks(
54+
markdown: string,
55+
sectionPattern: RegExp,
56+
): string[] {
57+
const lines = markdown.split("\n");
58+
const blocks: string[] = [];
59+
let inSection = false;
60+
let sectionLevel = 0;
61+
let inFence = false;
62+
let fenceLang = "";
63+
let current: string[] = [];
64+
65+
for (const line of lines) {
66+
const headingMatch = line.match(/^(#+)\s+(.*)$/);
67+
if (headingMatch && !inFence) {
68+
const level = headingMatch[1].length;
69+
const text = headingMatch[2];
70+
if (!inSection) {
71+
if (sectionPattern.test(text)) {
72+
inSection = true;
73+
sectionLevel = level;
74+
}
75+
} else if (level <= sectionLevel) {
76+
inSection = false;
77+
}
78+
continue;
79+
}
80+
if (!inSection) continue;
81+
82+
const fenceMatch = line.match(/^```(\w*)/);
83+
if (fenceMatch) {
84+
if (!inFence) {
85+
inFence = true;
86+
fenceLang = fenceMatch[1].toLowerCase();
87+
current = [];
88+
} else {
89+
if (/^(ts|tsx|typescript)$/.test(fenceLang)) {
90+
blocks.push(current.join("\n"));
91+
}
92+
inFence = false;
93+
fenceLang = "";
94+
}
95+
continue;
96+
}
97+
if (inFence) current.push(line);
98+
}
99+
return blocks;
100+
}
101+
102+
/**
103+
* Concatenate the extracted blocks into one ESM module, hoisting and
104+
* deduping `import` statements, then appending an `export` clause
105+
* exposing the identifiers we want the test to inspect.
106+
*/
107+
function buildModuleSource(blocks: string[]): string {
108+
const importLines = new Set<string>();
109+
const bodyChunks: string[] = [];
110+
111+
for (const block of blocks) {
112+
const blockLines = block.split("\n");
113+
const body: string[] = [];
114+
let i = 0;
115+
// Imports may span multiple lines if a `{ ... }` import is broken
116+
// across them, so accumulate until a line ends with a quote+semi.
117+
while (i < blockLines.length) {
118+
const line = blockLines[i];
119+
if (/^\s*import\b/.test(line)) {
120+
let stmt = line;
121+
while (!/["'];?\s*$/.test(stmt) && i + 1 < blockLines.length) {
122+
i += 1;
123+
stmt += "\n" + blockLines[i];
124+
}
125+
importLines.add(stmt.trim());
126+
i += 1;
127+
continue;
128+
}
129+
body.push(line);
130+
i += 1;
131+
}
132+
bodyChunks.push(body.join("\n"));
133+
}
134+
135+
return [
136+
"// AUTO-GENERATED from documentation code blocks.",
137+
"// Do not edit; edit the source markdown instead.",
138+
"/* eslint-disable */",
139+
...Array.from(importLines),
140+
"",
141+
bodyChunks.join("\n\n"),
142+
"",
143+
"export {",
144+
" signedActivity,",
145+
" inlineSignature,",
146+
" remoteAttestation,",
147+
" cidBytes,",
148+
" signatureBytes,",
149+
" keypair,",
150+
"};",
151+
"",
152+
].join("\n");
153+
}
154+
155+
async function loadExampleFrom(
156+
relPath: string,
157+
outputBasename: string,
158+
): Promise<ExtractedExample> {
159+
const markdown = readFileSync(resolve(ROOT, relPath), "utf-8");
160+
const blocks = extractSectionBlocks(markdown, /Cryptographic Signatures/i);
161+
if (blocks.length < 3) {
162+
throw new Error(
163+
`Expected at least 3 TypeScript blocks under "Cryptographic Signatures" in ${relPath}, found ${blocks.length}`,
164+
);
165+
}
166+
const source = buildModuleSource(blocks);
167+
mkdirSync(EXTRACT_DIR, { recursive: true });
168+
const outPath = resolve(EXTRACT_DIR, `${outputBasename}.ts`);
169+
writeFileSync(outPath, source, "utf-8");
170+
return (await import(pathToFileURL(outPath).href)) as ExtractedExample;
171+
}
172+
173+
const SOURCES: Array<[label: string, relPath: string, basename: string]> = [
174+
[
175+
"SKILL.md",
176+
".agents/skills/building-with-hypercerts-lexicons/SKILL.md",
177+
"signing-example-skill",
178+
],
179+
["README.md", "README.md", "signing-example-readme"],
180+
];
181+
182+
for (const [label, relPath, basename] of SOURCES) {
183+
describe(`signing example extracted from ${label}`, () => {
184+
let example: ExtractedExample;
185+
186+
it("extracts and runs the documented signing procedure without throwing", async () => {
187+
example = await loadExampleFrom(relPath, basename);
188+
expect(example).toBeDefined();
189+
});
190+
191+
it("produces a 36-byte CIDv1 with the dag-cbor/SHA-256 prefix", () => {
192+
expect(example.cidBytes).toBeInstanceOf(Uint8Array);
193+
expect(example.cidBytes.length).toBe(36);
194+
// CIDv1 + dag-cbor codec + SHA-256 multihash + 32-byte digest length
195+
expect(Array.from(example.cidBytes.slice(0, 4))).toEqual([
196+
0x01, 0x71, 0x12, 0x20,
197+
]);
198+
});
199+
200+
it("produces 64 raw r,s bytes from Secp256k1Keypair.sign()", () => {
201+
expect(example.signatureBytes).toBeInstanceOf(Uint8Array);
202+
expect(example.signatureBytes.length).toBe(64);
203+
});
204+
205+
it("builds an inline signature with the correct $type and shape", () => {
206+
expect(example.inlineSignature.$type).toBe(
207+
"app.certified.signature.defs#inline",
208+
);
209+
expect(example.inlineSignature.signature).toBe(example.signatureBytes);
210+
expect(example.inlineSignature.key).toMatch(/^did:[^:]+:.+#.+$/);
211+
});
212+
213+
it("builds a remote-attestation strongRef", () => {
214+
expect(example.remoteAttestation.$type).toBe(
215+
"com.atproto.repo.strongRef",
216+
);
217+
expect(example.remoteAttestation.uri).toMatch(/^at:\/\//);
218+
});
219+
220+
it("attaches both signatures to the final record", () => {
221+
expect(example.signedActivity.signatures).toHaveLength(2);
222+
expect(example.signedActivity.signatures[0]).toBe(
223+
example.inlineSignature,
224+
);
225+
expect(example.signedActivity.signatures[1]).toBe(
226+
example.remoteAttestation,
227+
);
228+
});
229+
230+
it("produces a record that passes lexicon validation", () => {
231+
// Replace the placeholder remote-attestation strongRef CID with a
232+
// syntactically-valid CID so the record can be validated; the
233+
// example uses "bafy..." which the lexicon validator rejects.
234+
const VALID_PROOF_CID =
235+
"bafyreie5737gdxlw5i64vngml6xvqeatqy3a4erphoqtso54z2eooh4zae";
236+
const validatable = {
237+
...example.signedActivity,
238+
signatures: [
239+
example.inlineSignature,
240+
{ ...example.remoteAttestation, cid: VALID_PROOF_CID },
241+
],
242+
};
243+
const result = Activity.validateMain(validatable);
244+
expect(result.success).toBe(true);
245+
});
246+
247+
it("produced signature verifies against the signing public key", async () => {
248+
const ok = await verifySignature(
249+
example.keypair.did(),
250+
example.cidBytes,
251+
example.signatureBytes,
252+
);
253+
expect(ok).toBe(true);
254+
});
255+
});
256+
}

0 commit comments

Comments
 (0)