Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/docs-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: docs-check

on:
pull_request:
push:
branches: [main]

jobs:
code-blocks:
name: Check doc code blocks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on checkout steps.

Both actions/checkout@v4 steps default to persisting the GITHUB_TOKEN credential in the workspace, which is unnecessary here since neither job pushes or authenticates further. Flagged by zizmor as credential persistence (artipacked).

🔒 Proposed fix
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
       - uses: actions/setup-node@v4

Also applies to: 25-25

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 13-13: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs-check.yml at line 13, Set persist-credentials to
false on each actions/checkout@v4 step in the docs workflow so the GITHUB_TOKEN
is not stored in the workspace. Update both checkout invocations in the workflow
jobs, since neither job needs to push or reuse credentials, and keep the fix
scoped to the checkout action configuration.

Source: Linters/SAST tools

- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn check-docs

build:
name: Build site
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn build
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build && node scripts/fix-markdown-links.js",
"check-docs": "node scripts/check-docs-code.mjs",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
Expand All @@ -30,8 +31,16 @@
"@docusaurus/faster": "3.10.1",
"@docusaurus/module-type-aliases": "3.10.1",
"@tailwindcss/postcss": "^4.3.1",
"@types/node": "^26.1.0",
"abstractionkit": "0.4.0",
"dotenv": "^17.4.2",
"ethers": "^6.17.0",
"safe-recovery-service-sdk": "^0.0.4",
"siwe": "^3.0.0",
"tailwindcss": "^4.3.1",
"typescript": "^6.0.3"
"typescript": "^6.0.3",
"viem": "^2.54.2",
"wagmi": "^3.6.21"
Comment on lines +34 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Do @types/node major version mismatches with the Node.js runtime cause type errors with commonly used Node APIs?

💡 Result:

Yes, major version mismatches between @types/node and the Node.js runtime can cause type errors, primarily due to how TypeScript handles global declarations [1][2]. When multiple packages in a project (or its dependencies) rely on different versions of @types/node, it often results in duplicate declarations of global objects (such as AbortSignal, Buffer, or global Node.js APIs) [3][1][2]. Because TypeScript does not support multiple conflicting declarations for the same global variable, this leads to the common TS2403 error (Subsequent variable declarations must have the same type) [1][2]. The implications of a mismatch generally fall into two categories: 1. Build-Time Type Errors: If your project or its dependencies use incompatible versions, you may encounter compilation failures because the global type definitions provided by @types/node clash [3][2]. This is frequently solved by aligning all dependencies to the same major version of @types/node [3][1]. 2. Inaccurate Type Checking: - If @types/node is older than your runtime, TypeScript will not recognize newer Node.js APIs, leading to compilation errors when you use them [4]. - If @types/node is newer than your runtime, TypeScript might permit the use of APIs that are not actually available in your current Node.js environment, which can lead to runtime crashes when the code is executed [4]. To maintain stability, it is recommended to match the major version of @types/node to the major version of the Node.js environment being used [4]. In complex projects, ensure that sub-dependencies do not pull in conflicting versions of @types/node, often by using resolutions (in npm/yarn) or overrides to enforce a single version [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json ==\n'
cat -n package.json | sed -n '1,140p'

printf '\n== docs-check workflow ==\n'
cat -n .github/workflows/docs-check.yml | sed -n '1,120p'

printf '\n== search for node/version references ==\n'
rg -n --hidden --glob '!**/node_modules/**' 'setup-node|node-version|`@types/node`|engines' .github package.json

Repository: candidelabs/developer-docs

Length of output: 3760


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tsconfig files ==\n'
fd -a 'tsconfig*.json' .

printf '\n== docs-check script ==\n'
cat -n scripts/check-docs-code.mjs | sed -n '1,260p'

printf '\n== tsconfig / type references ==\n'
rg -n --hidden --glob '!**/node_modules/**' '"types"|`@types/node`|node:' tsconfig*.json . scripts package.json .github/workflows

Repository: candidelabs/developer-docs

Length of output: 12515


Align @types/node with the CI Node version. package.json pins @types/node to ^26.1.0, but .github/workflows/docs-check.yml runs the docs typecheck on Node 22. That makes the docs checker use a newer API surface than CI actually provides; either pin @types/node to 22 or bump the workflow to 26.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 34 - 43, The docs typecheck is using a mismatched
Node API surface because package.json pins `@types/node` to a newer major than the
Node version used by the docs-check workflow. Update the `@types/node` dependency
in package.json to match the CI Node runtime, or adjust the docs-check workflow
to run on the same Node major; use the `@types/node` entry and the docs-check
workflow as the two places to keep aligned.

},
"browserslist": {
"production": [
Expand Down
199 changes: 199 additions & 0 deletions scripts/check-docs-code.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env node
/**
* Static checker for code blocks in the docs.
*
* - ```json blocks must parse with JSON.parse
* - ```ts / ```typescript / ```tsx blocks must typecheck with tsc --noEmit
* against the pinned abstractionkit version in devDependencies
*
* Nothing is executed: no network, no keys, no chain. This catches references
* to renamed SDK methods, wrong field names, syntax errors, and invalid JSON.
*
* Escape hatches:
* - Put `<!-- docs-check: skip -->` on its own line directly above a fence to
* exempt that one block (use for intentionally elided snippets).
* - Pre-existing failures live in scripts/docs-check-baseline.json, keyed by a
* hash of the block's content. CI fails only on NEW failures. Fixing a block
* changes its hash, so fixed blocks leave the baseline automatically; run
* with --update-baseline to rewrite the file (also prunes stale entries).
*/

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const docsDir = path.join(repoRoot, "docs");
const baselinePath = path.join(repoRoot, "scripts", "docs-check-baseline.json");
const tmpDir = path.join(repoRoot, "node_modules", ".cache", "docs-check");
const updateBaseline = process.argv.includes("--update-baseline");

const TS_LANGS = new Set(["ts", "typescript", "tsx"]);
const SKIP_MARKER = /<!--\s*docs-check:\s*skip\s*-->/;

// ---------------------------------------------------------------- extraction

function* walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) yield* walk(full);
else if (/\.mdx?$/.test(entry.name)) yield full;
}
}

function extractBlocks(filePath) {
const lines = fs.readFileSync(filePath, "utf8").split("\n");
const blocks = [];
let open = null; // { lang, indent, startLine, content: [] }
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (open) {
if (line.slice(open.indent.length).trimEnd() === "```" || line.trim() === "```") {
blocks.push({ ...open, content: open.content.join("\n") });
open = null;
} else {
open.content.push(line.startsWith(open.indent) ? line.slice(open.indent.length) : line);
}
continue;
}
const fence = line.match(/^(\s*)```(\w+)/);
if (!fence) continue;
const skip = i > 0 && SKIP_MARKER.test(lines[i - 1]);
const lang = fence[2].toLowerCase();
if (!skip && (lang === "json" || TS_LANGS.has(lang))) {
open = { lang, indent: fence[1], startLine: i + 2, content: [] };
} else {
// still consume the fence so nested content is not re-scanned
open = { lang: null, indent: fence[1], startLine: i + 2, content: [] };
}
}
return blocks.filter((b) => b.lang !== null);
}

function blockHash(lang, content) {
return createHash("sha256").update(`${lang}\n${content}`).digest("hex").slice(0, 12);
}

// ------------------------------------------------------------------- checks

const failures = []; // { file, line, hash, lang, message }
const tsBlocks = []; // { scratchFile, docFile, startLine, hash }

fs.rmSync(tmpDir, { recursive: true, force: true });
fs.mkdirSync(tmpDir, { recursive: true });

let total = 0;
for (const file of walk(docsDir)) {
const rel = path.relative(repoRoot, file);
for (const block of extractBlocks(file)) {
total++;
const hash = blockHash(block.lang, block.content);
if (block.lang === "json") {
try {
JSON.parse(block.content);
} catch (err) {
failures.push({ file: rel, line: block.startLine, hash, lang: "json", message: err.message });
}
} else {
const ext = block.lang === "tsx" ? "tsx" : "ts";
const scratchFile = path.join(tmpDir, `block-${tsBlocks.length}.${ext}`);
// `export {}` makes the block a module: imports and top-level await work,
// and declarations cannot collide across blocks.
fs.writeFileSync(scratchFile, `${block.content}\nexport {};\n`);
tsBlocks.push({ scratchFile, docFile: rel, startLine: block.startLine, hash });
}
}
}

function runTsc(files, configName) {
const tsconfig = {
compilerOptions: {
target: "ES2022",
module: "ESNext",
moduleResolution: "Bundler",
jsx: "react-jsx",
lib: ["ES2022", "DOM"],
types: ["node"],
strict: false,
noEmit: true,
skipLibCheck: true,
esModuleInterop: true,
},
files,
};
const configPath = path.join(tmpDir, configName);
fs.writeFileSync(configPath, JSON.stringify(tsconfig, null, 2));
let output = "";
try {
execFileSync("npx", ["tsc", "-p", configPath, "--pretty", "false"], {
cwd: repoRoot,
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
});
} catch (err) {
output = `${err.stdout ?? ""}${err.stderr ?? ""}`;
}
const errors = new Map(); // scratchFile -> [{ line, message }]
for (const line of output.split("\n")) {
const m = line.match(/^(.*?)\((\d+),(\d+)\): (error TS\d+: .*)$/);
if (!m) continue;
const file = path.resolve(repoRoot, m[1]);
const entry = errors.get(file) ?? [];
entry.push({ line: Number(m[2]), message: m[4] });
errors.set(file, entry);
}
return errors;
}
Comment on lines +110 to +148

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== scripts/check-docs-code.mjs (relevant section) ==\n'
nl -ba scripts/check-docs-code.mjs | sed -n '1,220p'

printf '\n== package.json / lockfile references to typescript, npx, and node typings ==\n'
for f in package.json npm-shrinkwrap.json package-lock.json pnpm-lock.yaml yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '"typescript"|`@types/node`|npx|tsc' "$f" || true
  fi
done

Repository: candidelabs/developer-docs

Length of output: 258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== scripts/check-docs-code.mjs (relevant section) ==\n'
cat -n scripts/check-docs-code.mjs | sed -n '1,220p'

printf '\n== package.json ==\n'
cat -n package.json | sed -n '1,260p'

printf '\n== lockfile references to typescript / `@types/node` ==\n'
for f in package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '"typescript"|`@types/node`|typescript@|`@types/node`@' "$f" || true
  fi
done

Repository: candidelabs/developer-docs

Length of output: 13102


Fail closed when tsc invocation fails scripts/check-docs-code.mjs:110-148 — if npx/tsc exits before emitting error TS... lines, runTsc returns an empty map and the caller treats every TS block as passing. Surface that as a hard error instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-docs-code.mjs` around lines 110 - 148, The runTsc helper is
currently swallowing failed tsc/npx executions when no parseable TS errors are
emitted, causing an empty Map to be treated as success. Update runTsc so that
the execFileSync failure path in the try/catch for npx tsc cannot silently
return no errors: capture the thrown error state and rethrow or otherwise signal
a hard failure if output contains no matched TS diagnostics. Keep the change
localized to runTsc and its output parsing logic so callers no longer treat
invocation failures as passing.


if (tsBlocks.length > 0) {
// Two passes: when any file in the program has syntax errors, tsc skips
// semantic checking for the whole program. Pass 1 catches syntax failures;
// pass 2 re-checks the syntactically clean blocks for semantic errors.
const pass1 = runTsc(tsBlocks.map((b) => b.scratchFile), "tsconfig.json");
const cleanFiles = tsBlocks.map((b) => b.scratchFile).filter((f) => !pass1.has(f));
const pass2 = cleanFiles.length > 0 ? runTsc(cleanFiles, "tsconfig-pass2.json") : new Map();

for (const block of tsBlocks) {
const errors = pass1.get(block.scratchFile) ?? pass2.get(block.scratchFile);
if (!errors) continue;
failures.push({
file: block.docFile,
line: block.startLine + errors[0].line - 1,
hash: block.hash,
lang: "ts",
message: errors
.map((e) => `L${block.startLine + e.line - 1}: ${e.message}`)
.join("\n "),
});
}
}

// ----------------------------------------------------------------- baseline

const baseline = fs.existsSync(baselinePath) ? JSON.parse(fs.readFileSync(baselinePath, "utf8")) : {};
const failingHashes = new Set(failures.map((f) => f.hash));
const newFailures = failures.filter((f) => !(f.hash in baseline));
const staleEntries = Object.keys(baseline).filter((h) => !failingHashes.has(h));

if (updateBaseline) {
const next = {};
for (const f of failures) next[f.hash] = `${f.file}:${f.line} (${f.lang})`;
fs.writeFileSync(baselinePath, JSON.stringify(next, null, 2) + "\n");
console.log(`Baseline rewritten: ${failures.length} known failure(s).`);
process.exit(0);
}

console.log(`Checked ${total} code block(s): ${total - failures.length} pass, ${failures.length - newFailures.length} baselined, ${newFailures.length} new failure(s).`);
if (staleEntries.length > 0) {
console.log(`${staleEntries.length} baseline entr(ies) no longer fail; run --update-baseline to prune.`);
}

if (newFailures.length > 0) {
console.error("\nNew failures (fix the block, or add `<!-- docs-check: skip -->` above the fence for intentional elision):\n");
for (const f of newFailures) {
console.error(` ${f.file}:${f.line} [${f.lang}] (${f.hash})\n ${f.message}\n`);
}
process.exit(1);
}
Loading
Loading