diff --git a/.github/scripts/process-images.js b/.github/scripts/process-images.js
deleted file mode 100644
index 5b9edcbb..00000000
--- a/.github/scripts/process-images.js
+++ /dev/null
@@ -1,492 +0,0 @@
-const { S3Client } = require('@aws-sdk/client-s3');
-const { Upload } = require('@aws-sdk/lib-storage');
-const axios = require('axios');
-const sharp = require('sharp');
-const { v4: uuidv4 } = require('uuid');
-const crypto = require('crypto');
-const path = require('path');
-const fs = require('fs');
-
-// ============================================================================
-// CONFIGURATION CONSTANTS
-// ============================================================================
-const MAX_FILE_SIZE = 10 * 1024 * 1024;
-const MAX_WIDTH = 4096;
-const MAX_HEIGHT = 4096;
-const MAX_PIXELS = 16777216;
-const MAX_COMPRESSION_RATIO = 50;
-
-// Allowed image formats
-const ALLOWED_FORMATS = ['jpeg', 'jpg', 'png'];
-
-// Request timeout in milliseconds (30 seconds)
-const REQUEST_TIMEOUT = 30000;
-
-// Maximum images per request (rate limiting)
-const MAX_IMAGES_PER_REQUEST = 10;
-
-// Allowed Content-Type prefixes for image responses
-const ALLOWED_CONTENT_TYPES = ['image/jpeg', 'image/png', 'image/jpg'];
-
-// Only allowed hostname for GitHub PR comment images
-const ALLOWED_HOSTNAME = 'private-user-images.githubusercontent.com';
-
-// ============================================================================
-// AWS S3 CLIENT INITIALIZATION
-// ============================================================================
-const s3Client = new S3Client({
- region: process.env.AWS_REGION
-});
-
-// ============================================================================
-// URL SANITIZATION HELPER
-// ============================================================================
-// Redacts sensitive information from URLs for logging
-function sanitizeUrlForLogging(url) {
- try {
- const urlObj = new URL(url);
- const sensitiveParams = ['jwt', 'token', 'key', 'secret', 'auth', 'signature', 'sig'];
-
- for (const param of sensitiveParams) {
- if (urlObj.searchParams.has(param)) {
- urlObj.searchParams.set(param, '[REDACTED]');
- }
- }
-
- // Also check for JWT-like patterns in any parameter
- for (const [key, value] of urlObj.searchParams.entries()) {
- if (value && value.length > 50 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
- urlObj.searchParams.set(key, '[REDACTED_JWT]');
- }
- }
-
- return urlObj.toString();
- } catch {
- // If URL parsing fails, do basic redaction
- return url.replace(/([?&](jwt|token|key|secret|auth|signature|sig)=)[^&]+/gi, '$1[REDACTED]');
- }
-}
-
-// ============================================================================
-// IMAGE DOWNLOAD FUNCTION
-// ============================================================================
-// Downloads the image from the URL
-async function downloadImage(url) {
- const safeLogUrl = sanitizeUrlForLogging(url);
- console.log(`Downloading: ${safeLogUrl.substring(0, 100)}...`);
-
- // Validate URL hostname before making request
- try {
- const urlObj = new URL(url);
- if (urlObj.hostname.toLowerCase() !== ALLOWED_HOSTNAME) {
- throw new Error(`Invalid hostname. Only ${ALLOWED_HOSTNAME} URLs are accepted.`);
- }
- if (urlObj.protocol !== 'https:') {
- throw new Error('Only HTTPS URLs are allowed.');
- }
- if (!urlObj.searchParams.has('jwt')) {
- throw new Error('URL missing required JWT token. Please copy the full image URL from GitHub.');
- }
- } catch (error) {
- if (error.message.includes('Invalid hostname') ||
- error.message.includes('Only HTTPS') ||
- error.message.includes('JWT token')) {
- throw error;
- }
- throw new Error(`Invalid URL format: ${error.message}`);
- }
-
- try {
- const headers = {
- 'User-Agent': 'GitHub-Image-Upload-Bot/1.0',
- 'Accept': 'image/*'
- };
-
- const response = await axios({
- url,
- method: 'GET',
- responseType: 'arraybuffer',
- maxContentLength: MAX_FILE_SIZE,
- maxBodyLength: MAX_FILE_SIZE,
- timeout: REQUEST_TIMEOUT,
- maxRedirects: 0,
- headers,
- validateStatus: (status) => status >= 200 && status < 300,
- beforeRedirect: (options, { headers: responseHeaders }) => {
- throw new Error('Redirects are not allowed for security reasons');
- }
- });
-
- // Validate Content-Type header to ensure we're receiving an image
- const contentType = response.headers['content-type'];
- if (contentType) {
- const normalizedContentType = contentType.toLowerCase().split(';')[0].trim();
- const isValidImageType = ALLOWED_CONTENT_TYPES.some(
- allowed => normalizedContentType === allowed
- );
-
- if (!isValidImageType) {
- throw new Error(`Invalid Content-Type: ${normalizedContentType}. Expected: ${ALLOWED_CONTENT_TYPES.join(', ')}`);
- }
- console.log(`Content-Type: ${normalizedContentType}`);
- } else {
- throw new Error('Missing Content-Type header in response');
- }
-
- const buffer = Buffer.from(response.data);
-
- if (buffer.length > MAX_FILE_SIZE) {
- throw new Error(`Image size (${(buffer.length / 1024 / 1024).toFixed(2)}MB) exceeds maximum allowed size (${MAX_FILE_SIZE / 1024 / 1024}MB)`);
- }
-
- if (buffer.length === 0) {
- throw new Error('Downloaded image is empty');
- }
-
- console.log(`✓ Downloaded ${(buffer.length / 1024).toFixed(2)}KB`);
- return buffer;
- } catch (error) {
- if (error.response) {
- const status = error.response.status;
-
- if (status === 400 || status === 404) {
- throw new Error(
- 'Image URL expired or invalid (HTTP ' + status + '). ' +
- 'GitHub image URLs expire in ~5 minutes. ' +
- 'Please: 1) Refresh the PR page, 2) Copy a fresh image URL, 3) Run /img-bot again immediately.'
- );
- }
- throw new Error(`HTTP ${status}: ${error.response.statusText || 'Request failed'}`);
- }
-
- if (error.code === 'ECONNABORTED') {
- throw new Error('Download timed out. Please try again.');
- }
-
- throw new Error(`Download failed: ${error.message}`);
- }
-}
-
-// ============================================================================
-// IMAGE VALIDATION FUNCTION
-// ============================================================================
-// Validates that the downloaded buffer is a valid image
-async function validateImage(buffer) {
- console.log('Validating image...');
-
- try {
- const sharpInstance = sharp(buffer, {
- limitInputPixels: MAX_PIXELS,
- sequentialRead: true,
- failOn: 'error'
- });
-
- const metadata = await sharpInstance.metadata();
-
- console.log('Image metadata:', {
- format: metadata.format,
- width: metadata.width,
- height: metadata.height,
- size: `${(buffer.length / 1024).toFixed(2)}KB`
- });
-
- // Validate format
- if (!ALLOWED_FORMATS.includes(metadata.format)) {
- throw new Error(`Invalid format: ${metadata.format}. Allowed: ${ALLOWED_FORMATS.join(', ')}`);
- }
-
- // Validate dimensions (prevents overly large images)
- if (!metadata.width || !metadata.height) {
- throw new Error('Unable to determine image dimensions');
- }
-
- if (metadata.width > MAX_WIDTH || metadata.height > MAX_HEIGHT) {
- throw new Error(`Image dimensions ${metadata.width}x${metadata.height} exceed maximum ${MAX_WIDTH}x${MAX_HEIGHT}`);
- }
-
- if (metadata.width < 1 || metadata.height < 1) {
- throw new Error('Invalid image dimensions');
- }
-
- // Check total pixel count
- const totalPixels = metadata.width * metadata.height;
- if (totalPixels > MAX_PIXELS) {
- throw new Error(`Image pixel count ${totalPixels.toLocaleString()} exceeds maximum ${MAX_PIXELS.toLocaleString()}`);
- }
-
- const channels = metadata.channels || 4;
- const bitsPerChannel = metadata.depth === 'uchar' ? 8 : (metadata.depth === 'ushort' ? 16 : 8);
- const bytesPerPixel = channels * (bitsPerChannel / 8);
- const estimatedUncompressedSize = metadata.width * metadata.height * bytesPerPixel;
- const compressionRatio = estimatedUncompressedSize / buffer.length;
-
- console.log(`Compression ratio: ${compressionRatio.toFixed(1)}:1`);
-
- if (compressionRatio > MAX_COMPRESSION_RATIO) {
- throw new Error(
- `Suspicious compression ratio (${compressionRatio.toFixed(1)}:1) exceeds maximum allowed (${MAX_COMPRESSION_RATIO}:1). ` +
- `This may indicate a decompression bomb.`
- );
- }
-
- console.log('✓ Image validation passed');
- return metadata;
- } catch (error) {
- const safeMessage = error.message
- .replace(/\/[^\s]+/g, '[PATH]')
- .substring(0, 200);
- throw new Error(`Image validation failed: ${safeMessage}`);
- }
-}
-
-// ============================================================================
-// EXIF STRIPPING FUNCTION
-// ============================================================================
-// Strips EXIF metadata from images
-async function stripExifMetadata(buffer, format) {
- console.log('Stripping EXIF metadata...');
-
- try {
- const sharpInstance = sharp(buffer, {
- limitInputPixels: MAX_PIXELS,
- sequentialRead: true
- });
-
- let processed = sharpInstance.rotate();
-
- if (format === 'png') {
- processed = processed.png({
- compressionLevel: 9,
- effort: 10
- });
- } else {
- processed = processed.jpeg({
- quality: 95,
- mozjpeg: true
- });
- }
-
- const strippedBuffer = await processed.toBuffer();
-
- console.log(`✓ EXIF stripped. Original: ${(buffer.length / 1024).toFixed(2)}KB, New: ${(strippedBuffer.length / 1024).toFixed(2)}KB`);
- return strippedBuffer;
- } catch (error) {
- console.warn(`Warning: Could not strip EXIF metadata: ${error.message}`);
- return buffer;
- }
-}
-
-// ============================================================================
-// FILENAME GENERATION FUNCTION
-// ============================================================================
-// Generates a unique filename for the uploaded image
-function generateUniqueFilename(originalUrl, format) {
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
-
- // Use full UUID for better collision resistance
- const uniqueId = uuidv4();
- const randomBytes = crypto.randomBytes(16).toString('hex');
- const hash = crypto.createHash('sha256')
- .update(originalUrl)
- .update(randomBytes)
- .update(Date.now().toString())
- .digest('hex')
- .substring(0, 12);
-
- const sanitizedFormat = format.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
-
- const safeFormats = ['jpeg', 'jpg', 'png'];
- if (!safeFormats.includes(sanitizedFormat)) {
- throw new Error(`Invalid format for filename: ${format}`);
- }
-
- return `${timestamp}_${uniqueId}_${hash}.${sanitizedFormat}`;
-}
-
-// ============================================================================
-// S3 UPLOAD FUNCTION
-// ============================================================================
-// Uploads the image buffer to AWS S3
-async function uploadToS3(buffer, filename, contentType) {
- console.log(`Uploading to S3: ${filename}`);
-
- try {
- const bucket = process.env.AWS_S3_BUCKET;
- const region = process.env.AWS_REGION;
-
- if (!bucket || !region) {
- throw new Error('AWS_S3_BUCKET and AWS_REGION environment variables are required');
- }
-
- const upload = new Upload({
- client: s3Client,
- params: {
- Bucket: bucket,
- Key: `images/${filename}`,
- Body: buffer,
- ContentType: contentType,
- CacheControl: 'public, max-age=31536000, immutable',
- ContentDisposition: `inline; filename="${filename}"`,
- Metadata: {
- 'uploaded-by': 'img-bot',
- 'upload-timestamp': new Date().toISOString()
- }
- }
- });
-
- await upload.done();
-
- const publicUrl = `https://${bucket}.s3.${region}.amazonaws.com/images/${filename}`;
-
- console.log(`✓ Uploaded to S3: ${publicUrl}`);
- return publicUrl;
- } catch (error) {
- const safeMessage = error.message
- .replace(/arn:[^\s]+/gi, '[ARN]')
- .replace(/[a-z0-9-]+\.s3\.[a-z0-9-]+\.amazonaws\.com/gi, '[S3_ENDPOINT]')
- .replace(/s3:\/\/[^\s]+/gi, '[S3_PATH]')
- .substring(0, 200);
-
- throw new Error(`S3 upload failed: ${safeMessage}`);
- }
-}
-
-// ============================================================================
-// MAIN IMAGE PROCESSING FUNCTION
-// ============================================================================
-// Processes a single image: download, validate, strip EXIF, upload
-async function processImage(url, index) {
- const safeLogUrl = sanitizeUrlForLogging(url).substring(0, 80) + '...';
-
- try {
- const buffer = await downloadImage(url);
-
- const metadata = await validateImage(buffer);
-
- const strippedBuffer = await stripExifMetadata(buffer, metadata.format);
-
- const filename = generateUniqueFilename(url, metadata.format);
-
- const s3Url = await uploadToS3(
- strippedBuffer,
- filename,
- `image/${metadata.format}`
- );
-
- return {
- success: true,
- originalUrl: safeLogUrl,
- s3Url: s3Url,
- filename: filename,
- metadata: {
- format: metadata.format,
- width: metadata.width,
- height: metadata.height,
- size: `${(strippedBuffer.length / 1024).toFixed(2)}KB`
- }
- };
- } catch (error) {
- // We don't throw the error here so other images can still be processed
- return {
- success: false,
- originalUrl: safeLogUrl,
- error: error.message.substring(0, 300)
- };
- }
-}
-
-// ============================================================================
-// MAIN ENTRY POINT
-// ============================================================================
-// Reads URLs from file and processes them sequentially
-async function main() {
- let urls;
-
- try {
- const urlsPath = process.env.URLS_FILE_PATH || 'urls.json';
-
- if (!fs.existsSync(urlsPath)) {
- throw new Error(`URLs file not found: ${urlsPath}`);
- }
-
- const rawData = fs.readFileSync(urlsPath, 'utf8');
- urls = JSON.parse(rawData);
-
- if (!Array.isArray(urls)) {
- throw new Error('URLs file does not contain an array');
- }
-
- if (urls.length === 0) {
- throw new Error('URLs array is empty');
- }
-
- if (urls.length > MAX_IMAGES_PER_REQUEST) {
- throw new Error(
- `Too many images (${urls.length}). Maximum allowed per request: ${MAX_IMAGES_PER_REQUEST}`
- );
- }
-
- for (const url of urls) {
- if (typeof url !== 'string' || !url.startsWith('https://')) {
- throw new Error('Invalid URL format in input array');
- }
- }
-
- console.log(`Processing ${urls.length} image(s)...`);
- } catch (error) {
- console.error('Failed to read/parse URLs file:', error.message);
- // Output error result so workflow can parse and report it
- console.log('\nRESULTS:', JSON.stringify([{
- success: false,
- originalUrl: 'unknown',
- error: `Failed to read/parse URLs: ${error.message.substring(0, 200)}`
- }], null, 2));
- process.exit(1);
- }
-
- // Process each URL sequentially
- const results = [];
- for (let i = 0; i < urls.length; i++) {
- const url = urls[i];
- const safeLogUrl = sanitizeUrlForLogging(url).substring(0, 60);
- console.log(`\n[${i + 1}/${urls.length}] Processing: ${safeLogUrl}...`);
-
- const result = await processImage(url, i);
- results.push(result);
-
- if (result.success) {
- console.log(`✓ Success: ${result.filename}`);
- } else {
- console.error(`✗ Failed: ${result.error}`);
- }
- }
-
- // Summary
- const successCount = results.filter(r => r.success).length;
- const failCount = results.filter(r => !r.success).length;
- console.log(`\n========================================`);
- console.log(`Summary: ${successCount} succeeded, ${failCount} failed`);
- console.log(`========================================`);
-
- // Always output RESULTS, even if empty (for workflow parsing)
- console.log('\nRESULTS:', JSON.stringify(results, null, 2));
-
- // Exit with code 1 only if all images failed
- const allFailed = results.length > 0 && results.every(r => !r.success);
- if (allFailed) {
- console.error('\n✗ All images failed to process');
- process.exit(1);
- }
-}
-
-// Run main function and handle any unhandled errors
-main().catch(error => {
- console.error('Fatal error:', error.message);
- // Output empty results so workflow can still parse and report the error
- console.log('\nRESULTS:', JSON.stringify([{
- success: false,
- originalUrl: 'unknown',
- error: `Fatal error: ${error.message.substring(0, 200)}`
- }], null, 2));
- process.exit(1);
-});
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 30eac816..af8cb2ba 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -386,17 +386,6 @@ fits, for example in block-quotes.
> ⚠️ Please do not add images directly to the repository.
> This helps us avoid bloating the Git history and ensures all images follow our standardized storage and delivery method.
-- Adding images is welcome and encouraged.
- Please follow the steps below to include them correctly:
-
- 1. After making your changes and opening a PR, add the images you want to include in the PR's comments
- (by uploading them directly)
- 2. During the review, a maintainer will upload your images to our S3 bucket and reply with the links you should use.
- 3. Once you receive the new links, update your PR to add the images' links.
-
-> ⚠️ Please do not add images directly to the repository.
-> This helps us avoid bloating the Git history and ensures all images follow our standardized storage and delivery method.
-
### Linking resources
- Prefer descriptive names for external links (e.g., `inevitableeth.com` instead of "this wiki").
diff --git a/components/governance-sdlc/ChecklistItem.css b/components/governance-sdlc/ChecklistItem.css
deleted file mode 100644
index cd6f20a9..00000000
--- a/components/governance-sdlc/ChecklistItem.css
+++ /dev/null
@@ -1,70 +0,0 @@
-/* ChecklistItem — interactive checkbox + bold title with plain-paragraph
- * description below. No nested list marker on the description.
- */
-
-.gov-checklist-item {
- margin: 12px 0 16px;
-}
-
-.gov-checklist-row {
- display: flex;
- align-items: baseline;
- gap: 10px;
- cursor: pointer;
- user-select: none;
-}
-
-.gov-checklist-checkbox {
- /* Large, obvious tap/click target. */
- width: 18px;
- height: 18px;
- margin: 0;
- flex-shrink: 0;
- /* Pull up so the box visually aligns with the title baseline. */
- transform: translateY(3px);
- cursor: pointer;
- accent-color: var(--color-primary, #4339db);
-}
-
-.gov-checklist-title {
- font-weight: 700;
- color: var(--color-text-strong);
- line-height: 1.45;
-}
-
-.gov-checklist-item.is-checked .gov-checklist-title {
- color: var(--color-text-muted);
- text-decoration: line-through;
-}
-
-.gov-checklist-desc {
- /* Indent to align with the title text (checkbox width + row gap). */
- margin: 4px 0 0 28px;
- color: var(--color-text-muted);
- line-height: 1.6;
-}
-
-/* Flatten any stray nested markers: MDX may occasionally wrap prose in a
- * single
,
, or similar. Guarantee no bullet shows up inside the
- * description.
- */
-.gov-checklist-desc ul,
-.gov-checklist-desc ol {
- list-style: none;
- padding-left: 0;
- margin: 0.5em 0;
-}
-
-.gov-checklist-desc p {
- margin: 0 0 0.6em;
-}
-
-.gov-checklist-desc p:last-child {
- margin-bottom: 0;
-}
-
-@media (max-width: 640px) {
- .gov-checklist-desc {
- margin-left: 24px;
- }
-}
diff --git a/components/governance-sdlc/ChecklistItem.tsx b/components/governance-sdlc/ChecklistItem.tsx
deleted file mode 100644
index dfa219fe..00000000
--- a/components/governance-sdlc/ChecklistItem.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { useEffect, useMemo, useState, type ReactNode } from "react";
-import "./ChecklistItem.css";
-
-/**
- * ChecklistItem — renders a governance SDLC checklist item as a single
- * interactive checkbox next to a bold title, with a plain-paragraph
- * description below (no nested list marker).
- *
- * State is persisted to localStorage so a signer / reviewer can work
- * through the list across sessions. The storage key is
- * `governance-sdlc-checklist:`, where `id` defaults to a slugified
- * version of the title.
- */
-
-const STORAGE_PREFIX = "governance-sdlc-checklist:";
-
-interface ChecklistItemProps {
- title: string;
- id?: string;
- children: ReactNode;
-}
-
-function slugify(input: string): string {
- return input
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .replace(/^-+|-+$/g, "");
-}
-
-export function ChecklistItem({ title, id, children }: ChecklistItemProps) {
- const slug = useMemo(() => id ?? slugify(title), [id, title]);
- const storageKey = `${STORAGE_PREFIX}${slug}`;
- const descId = `${slug}-desc`;
-
- const [checked, setChecked] = useState(false);
-
- // Hydrate from localStorage on mount (client-side only).
- useEffect(() => {
- if (typeof window === "undefined") return;
- try {
- const saved = window.localStorage.getItem(storageKey);
- if (saved === "true") setChecked(true);
- } catch {
- // Ignore — localStorage may be unavailable (privacy mode, SSR, etc.).
- }
- }, [storageKey]);
-
- const handleChange = (e: React.ChangeEvent) => {
- const next = e.target.checked;
- setChecked(next);
- if (typeof window === "undefined") return;
- try {
- window.localStorage.setItem(storageKey, next ? "true" : "false");
- } catch {
- // Ignore storage failures; UI state still updates for this session.
- }
- };
-
- return (
-
-
-
- {children}
-
-
- );
-}
diff --git a/docs/footer.tsx b/docs/footer.tsx
deleted file mode 100644
index 0ca1dd9c..00000000
--- a/docs/footer.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Footer() {
- return (
-
-
Security Frameworks © 2023 by Security Alliance, licensed under
CC BY-SA 4.0.
-
- )
-}
diff --git a/docs/pages/certs/sfc-devops-infrastructure.mdx b/docs/pages/certs/sfc-devops-infrastructure.mdx
index 12037d48..e3ccf569 100644
--- a/docs/pages/certs/sfc-devops-infrastructure.mdx
+++ b/docs/pages/certs/sfc-devops-infrastructure.mdx
@@ -193,8 +193,6 @@ import { TagList, AttributionList, CertList } from '../../../components'
# SFC - DevOps & Infrastructure
-*Revision {frontmatter.version} · Updated {frontmatter.revised} · [Changelog](/certs/changelog)*
-
The SEAL Framework Checklist (SFC) for DevOps & Infrastructure provides guidelines for securing development
environments, source code management, CI/CD pipelines, and cloud infrastructure. It covers governance, supply chain
security, deployment controls, and infrastructure access.
diff --git a/docs/pages/certs/sfc-incident-response.mdx b/docs/pages/certs/sfc-incident-response.mdx
index 1f6f7854..9442c18e 100644
--- a/docs/pages/certs/sfc-incident-response.mdx
+++ b/docs/pages/certs/sfc-incident-response.mdx
@@ -202,8 +202,6 @@ import { TagList, AttributionList, CertList } from '../../../components'
# SFC - Incident Response
-*Revision {frontmatter.version} · Updated {frontmatter.revised} · [Changelog](/certs/changelog)*
-
The SEAL Framework Checklist (SFC) for Incident Response provides structured guidelines to help teams remain prepared for
security incidents affecting blockchain protocols. It covers team structure, monitoring, alerting, and response
procedures.
diff --git a/docs/pages/certs/sfc-multisig-ops.mdx b/docs/pages/certs/sfc-multisig-ops.mdx
index 89109b2e..105a3425 100644
--- a/docs/pages/certs/sfc-multisig-ops.mdx
+++ b/docs/pages/certs/sfc-multisig-ops.mdx
@@ -255,8 +255,6 @@ import { TagList, AttributionList, CertList } from '../../../components'
# SFC - Multisig Operations
-*Revision {frontmatter.version} · Updated {frontmatter.revised} · [Changelog](/certs/changelog)*
-
The SEAL Framework Checklist (SFC) for Multisig Operations provides best practices for managing multisig wallets
securely. It covers governance, risk management, signer security, operational procedures, and emergency operations.
diff --git a/docs/pages/certs/sfc-treasury-ops.mdx b/docs/pages/certs/sfc-treasury-ops.mdx
index 91e574e5..3bffc9b5 100644
--- a/docs/pages/certs/sfc-treasury-ops.mdx
+++ b/docs/pages/certs/sfc-treasury-ops.mdx
@@ -316,8 +316,6 @@ import { TagList, AttributionList, CertList } from '../../../components'
# SFC - Treasury Operations
-*Revision {frontmatter.version} · Updated {frontmatter.revised} · [Changelog](/certs/changelog)*
-
The SEAL Framework Checklist (SFC) for Treasury Operations provides structured guidelines for securely managing and
operating an organization's treasury covering governance, access control, transaction security, monitoring, and vendor
management.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2df2a2e0..24812665 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -369,10 +369,6 @@ packages:
resolution: {integrity: sha512-L2bJerfuYOls2wEknm8FmynLtj/G7O4UqX9I/HznRggEW6i2yZIxagDetpVDNowpyavNHJ3SJtUFiyMiZc16Sw==}
engines: {node: '>=22.18.0'}
- '@cspell/cspell-worker@9.8.0':
- resolution: {integrity: sha512-W8FLdE3MXPLbWtAXciILQhk9CHd6Mt+HRjZHM8m+dwE1Bc2TAjUai8kIxsdhHUq58p7gYY2ekr5sg1uYOUgTAA==}
- engines: {node: '>=20.18'}
-
'@cspell/dict-ada@4.1.1':
resolution: {integrity: sha512-E+0YW9RhZod/9Qy2gxfNZiHJjCYFlCdI69br1eviQQWB8yOTJX0JHXLs79kOYhSW0kINPVUdvddEBe6Lu6CjGQ==}
@@ -596,145 +592,145 @@ packages:
'@esbuild/android-arm64@0.28.0':
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
engines: {node: '>=18'}
- cpu: [ppc64]
- os: [aix]
+ cpu: [arm64]
+ os: [android]
'@esbuild/android-arm@0.28.0':
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.0':
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
engines: {node: '>=18'}
- cpu: [arm]
+ cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.0':
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [android]
+ cpu: [arm64]
+ os: [darwin]
'@esbuild/darwin-x64@0.28.0':
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.0':
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [darwin]
+ cpu: [arm64]
+ os: [freebsd]
'@esbuild/freebsd-x64@0.28.0':
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.0':
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [freebsd]
+ cpu: [arm64]
+ os: [linux]
'@esbuild/linux-arm@0.28.0':
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.0':
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
engines: {node: '>=18'}
- cpu: [arm]
+ cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.0':
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
engines: {node: '>=18'}
- cpu: [ia32]
+ cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.0':
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
engines: {node: '>=18'}
- cpu: [loong64]
+ cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.0':
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
engines: {node: '>=18'}
- cpu: [mips64el]
+ cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.0':
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
engines: {node: '>=18'}
- cpu: [ppc64]
+ cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.0':
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
engines: {node: '>=18'}
- cpu: [riscv64]
+ cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.0':
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
engines: {node: '>=18'}
- cpu: [s390x]
+ cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.0':
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [linux]
+ cpu: [arm64]
+ os: [netbsd]
'@esbuild/netbsd-x64@0.28.0':
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.0':
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [netbsd]
+ cpu: [arm64]
+ os: [openbsd]
'@esbuild/openbsd-x64@0.28.0':
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.0':
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [openbsd]
+ cpu: [arm64]
+ os: [openharmony]
'@esbuild/sunos-x64@0.28.0':
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
engines: {node: '>=18'}
- cpu: [arm64]
- os: [openharmony]
+ cpu: [x64]
+ os: [sunos]
'@esbuild/win32-arm64@0.28.0':
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
engines: {node: '>=18'}
- cpu: [x64]
- os: [sunos]
+ cpu: [arm64]
+ os: [win32]
'@esbuild/win32-ia32@0.28.0':
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
engines: {node: '>=18'}
- cpu: [arm64]
+ cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.0':
@@ -743,12 +739,6 @@ packages:
cpu: [x64]
os: [win32]
- '@esbuild/win32-x64@0.27.7':
- resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
- engines: {node: '>=18'}
- cpu: [x64]
- os: [win32]
-
'@fast-csv/format@4.3.5':
resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==}
@@ -976,143 +966,6 @@ packages:
'@internationalized/number@3.6.7':
resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==}
- '@img/colour@1.0.0':
- resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
- engines: {node: '>=18'}
-
- '@img/sharp-darwin-arm64@0.34.5':
- resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [darwin]
-
- '@img/sharp-darwin-x64@0.34.5':
- resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [darwin]
-
- '@img/sharp-libvips-darwin-arm64@1.2.4':
- resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
- cpu: [arm64]
- os: [darwin]
-
- '@img/sharp-libvips-darwin-x64@1.2.4':
- resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
- cpu: [x64]
- os: [darwin]
-
- '@img/sharp-libvips-linux-arm64@1.2.4':
- resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-libvips-linux-arm@1.2.4':
- resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
- cpu: [arm]
- os: [linux]
-
- '@img/sharp-libvips-linux-ppc64@1.2.4':
- resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
- cpu: [ppc64]
- os: [linux]
-
- '@img/sharp-libvips-linux-riscv64@1.2.4':
- resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
- cpu: [riscv64]
- os: [linux]
-
- '@img/sharp-libvips-linux-s390x@1.2.4':
- resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
- cpu: [s390x]
- os: [linux]
-
- '@img/sharp-libvips-linux-x64@1.2.4':
- resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
- resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-libvips-linuxmusl-x64@1.2.4':
- resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-linux-arm64@0.34.5':
- resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-linux-arm@0.34.5':
- resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm]
- os: [linux]
-
- '@img/sharp-linux-ppc64@0.34.5':
- resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [ppc64]
- os: [linux]
-
- '@img/sharp-linux-riscv64@0.34.5':
- resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [riscv64]
- os: [linux]
-
- '@img/sharp-linux-s390x@0.34.5':
- resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [s390x]
- os: [linux]
-
- '@img/sharp-linux-x64@0.34.5':
- resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-linuxmusl-arm64@0.34.5':
- resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [linux]
-
- '@img/sharp-linuxmusl-x64@0.34.5':
- resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [linux]
-
- '@img/sharp-wasm32@0.34.5':
- resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [wasm32]
-
- '@img/sharp-win32-arm64@0.34.5':
- resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [arm64]
- os: [win32]
-
- '@img/sharp-win32-ia32@0.34.5':
- resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [ia32]
- os: [win32]
-
- '@img/sharp-win32-x64@0.34.5':
- resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
- engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
- cpu: [x64]
- os: [win32]
-
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -2288,9 +2141,6 @@ packages:
axios@1.18.1:
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
- axios@1.15.2:
- resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==}
-
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -2798,10 +2648,6 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
- delayed-stream@1.0.0:
- resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
- engines: {node: '>=0.4.0'}
-
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -2914,14 +2760,6 @@ packages:
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
engines: {node: '>=0.12'}
- es-object-atoms@1.1.1:
- resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
- engines: {node: '>= 0.4'}
-
- es-set-tostringtag@2.1.0:
- resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
- engines: {node: '>= 0.4'}
-
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -3018,10 +2856,6 @@ packages:
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
engines: {node: '>=18.0.0'}
- events@3.3.0:
- resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
- engines: {node: '>=0.8.x'}
-
exceljs@4.4.0:
resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==}
engines: {node: '>=8.3.0'}
@@ -3181,10 +3015,6 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
- get-proto@1.0.1:
- resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
- engines: {node: '>= 0.4'}
-
get-stream@5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
@@ -4478,9 +4308,6 @@ packages:
style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
- strnum@2.2.3:
- resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==}
-
style-to-js@1.1.21:
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
@@ -5777,9 +5604,6 @@ snapshots:
'@esbuild/win32-x64@0.28.0':
optional: true
- '@esbuild/win32-x64@0.27.7':
- optional: true
-
'@fast-csv/format@4.3.5':
dependencies:
'@types/node': 14.18.63
@@ -5991,102 +5815,6 @@ snapshots:
dependencies:
'@swc/helpers': 0.5.23
- '@img/colour@1.0.0': {}
-
- '@img/sharp-darwin-arm64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.2.4
- optional: true
-
- '@img/sharp-darwin-x64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.2.4
- optional: true
-
- '@img/sharp-libvips-darwin-arm64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-darwin-x64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linux-arm64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linux-arm@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linux-ppc64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linux-riscv64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linux-s390x@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linux-x64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linuxmusl-arm64@1.2.4':
- optional: true
-
- '@img/sharp-libvips-linuxmusl-x64@1.2.4':
- optional: true
-
- '@img/sharp-linux-arm64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.2.4
- optional: true
-
- '@img/sharp-linux-arm@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.2.4
- optional: true
-
- '@img/sharp-linux-ppc64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.2.4
- optional: true
-
- '@img/sharp-linux-riscv64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-riscv64': 1.2.4
- optional: true
-
- '@img/sharp-linux-s390x@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.2.4
- optional: true
-
- '@img/sharp-linux-x64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.2.4
- optional: true
-
- '@img/sharp-linuxmusl-arm64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.2.4
- optional: true
-
- '@img/sharp-linuxmusl-x64@0.34.5':
- optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.2.4
- optional: true
-
- '@img/sharp-wasm32@0.34.5':
- dependencies:
- '@emnapi/runtime': 1.8.1
- optional: true
-
- '@img/sharp-win32-arm64@0.34.5':
- optional: true
-
- '@img/sharp-win32-ia32@0.34.5':
- optional: true
-
- '@img/sharp-win32-x64@0.34.5':
- optional: true
-
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -7449,14 +7177,6 @@ snapshots:
- debug
- supports-color
- axios@1.15.2:
- dependencies:
- follow-redirects: 1.16.0
- form-data: 4.0.5
- proxy-from-env: 2.1.0
- transitivePeerDependencies:
- - debug
-
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -7998,8 +7718,6 @@ snapshots:
delayed-stream@1.0.0: {}
- delayed-stream@1.0.0: {}
-
depd@2.0.0: {}
dequal@2.0.3: {}
@@ -8106,17 +7824,6 @@ snapshots:
d: 1.0.2
ext: 1.7.0
- es-object-atoms@1.1.1:
- dependencies:
- es-errors: 1.3.0
-
- es-set-tostringtag@2.1.0:
- dependencies:
- es-errors: 1.3.0
- get-intrinsic: 1.3.0
- has-tostringtag: 1.0.2
- hasown: 2.0.3
-
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -8160,35 +7867,6 @@ snapshots:
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
- esbuild@0.27.7:
- optionalDependencies:
- '@esbuild/aix-ppc64': 0.27.7
- '@esbuild/android-arm': 0.27.7
- '@esbuild/android-arm64': 0.27.7
- '@esbuild/android-x64': 0.27.7
- '@esbuild/darwin-arm64': 0.27.7
- '@esbuild/darwin-x64': 0.27.7
- '@esbuild/freebsd-arm64': 0.27.7
- '@esbuild/freebsd-x64': 0.27.7
- '@esbuild/linux-arm': 0.27.7
- '@esbuild/linux-arm64': 0.27.7
- '@esbuild/linux-ia32': 0.27.7
- '@esbuild/linux-loong64': 0.27.7
- '@esbuild/linux-mips64el': 0.27.7
- '@esbuild/linux-ppc64': 0.27.7
- '@esbuild/linux-riscv64': 0.27.7
- '@esbuild/linux-s390x': 0.27.7
- '@esbuild/linux-x64': 0.27.7
- '@esbuild/netbsd-arm64': 0.27.7
- '@esbuild/netbsd-x64': 0.27.7
- '@esbuild/openbsd-arm64': 0.27.7
- '@esbuild/openbsd-x64': 0.27.7
- '@esbuild/openharmony-arm64': 0.27.7
- '@esbuild/sunos-x64': 0.27.7
- '@esbuild/win32-arm64': 0.27.7
- '@esbuild/win32-ia32': 0.27.7
- '@esbuild/win32-x64': 0.27.7
-
escalade@3.2.0: {}
escape-carriage@1.3.1: {}
@@ -8273,8 +7951,6 @@ snapshots:
dependencies:
eventsource-parser: 3.1.0
- events@3.3.0: {}
-
exceljs@4.4.0:
dependencies:
archiver: 5.3.2
@@ -8461,11 +8137,6 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
- get-proto@1.0.1:
- dependencies:
- dunder-proto: 1.0.1
- es-object-atoms: 1.1.1
-
get-stream@5.2.0:
dependencies:
pump: 3.0.3
@@ -10271,8 +9942,6 @@ snapshots:
style-mod@4.1.3: {}
- strnum@2.2.3: {}
-
style-to-js@1.1.21:
dependencies:
style-to-object: 1.0.14
@@ -10831,4 +10500,4 @@ snapshots:
zod@4.4.3: {}
- zwitch@2.0.4: {}
+ zwitch@2.0.4: {}
\ No newline at end of file
diff --git a/utils/generate-llms.js b/utils/generate-llms.js
deleted file mode 100644
index dc08583a..00000000
--- a/utils/generate-llms.js
+++ /dev/null
@@ -1,367 +0,0 @@
-/*
- Generates llms.txt files following the llms.txt standard (https://llmstxt.org/)
- - llms.txt: thin routing index listing all frameworks with descriptions and page topics
- - llms/{framework}.txt: framework index — overview content + links to all per-page files
- - llms/{framework}/{page}.txt: one file per sidebar page with full stripped markdown content
- - Page order follows the sidebar order defined in vocs.config.tsx
- - Runs post-build and writes to the dist directory
-*/
-
-const fs = require('fs');
-const path = require('path');
-const matter = require('gray-matter');
-
-const PROD_URL = 'https://frameworks.securityalliance.org';
-const BASE_URL = process.env.CF_PAGES_BRANCH === 'main'
- ? PROD_URL
- : (process.env.CF_PAGES_URL || PROD_URL);
-const workspaceRoot = process.cwd();
-const PAGES_DIR = path.join(workspaceRoot, 'docs', 'pages');
-const isMainBranch = process.env.CF_PAGES_BRANCH === 'main';
-
-function findDistDir() {
- const candidates = [
- path.join(workspaceRoot, 'docs', 'dist'),
- path.join(workspaceRoot, 'dist'),
- ];
- return candidates.find((dir) => fs.existsSync(dir));
-}
-
-// Returns all sidebar links in document order, filtered to a specific folder prefix.
-// Tracks brace depth so a dev: true flag on a parent block is inherited by all child links.
-function getSidebarLinksForFolder(folderName) {
- const configPath = path.join(workspaceRoot, 'vocs.config.tsx');
- if (!fs.existsSync(configPath)) return [];
-
- const lines = fs.readFileSync(configPath, 'utf8').split('\n');
- const links = [];
- let depth = 0;
- const devDepths = new Set(); // brace depths at which a parent block carries dev: true
-
- for (const line of lines) {
- if (isMainBranch) {
- const opens = (line.match(/\{/g) || []).length;
- const closes = (line.match(/\}/g) || []).length;
- const newDepth = depth + opens - closes;
-
- // Pop dev markers for blocks we're closing
- if (newDepth < depth) {
- for (const d of devDepths) {
- if (d > newDepth) devDepths.delete(d);
- }
- }
- depth = newDepth;
-
- // If this line carries dev: true but is not a link item, it's a container flag - inherit downward
- if (line.includes('dev:') && line.includes('true') && !line.match(/link:/)) {
- devDepths.add(depth);
- }
- }
-
- const match = line.match(/link:\s*(['"])(\/[^'"]+)\1/);
- if (!match) continue;
- const link = match[2];
-
- // Skip if the item itself has dev: true, or if any ancestor block does
- if (isMainBranch && ((line.includes('dev:') && line.includes('true')) || devDepths.size > 0)) continue;
-
- if (link.startsWith(`/${folderName}/`)) {
- links.push(link);
- }
- }
-
- return links;
-}
-
-function linkToFilePath(link) {
- const base = path.join(PAGES_DIR, link);
- for (const candidate of [`${base}.mdx`, `${base}.md`, path.join(base, 'index.mdx')]) {
- if (fs.existsSync(candidate)) return candidate;
- }
- return null;
-}
-
-function getPageUrl(filePath) {
- return filePath
- .replace(PAGES_DIR, '')
- .replace(/\.(mdx|md)$/, '')
- .replace(/\/index$/, '');
-}
-
-function stripSiteSuffix(title) {
- return title ? title.replace(/\s*\|.*$/, '').trim() : '';
-}
-
-const ACRONYMS = new Set(['ai', 'iam', 'ens', 'dprk', 'dns', 'it']);
-const SPECIAL_CASES = { opsec: 'OpSec', devsecops: 'DevSecOps' };
-
-function toTitleCase(slug) {
- return slug
- .split('-')
- .map((w) => {
- if (SPECIAL_CASES[w]) return SPECIAL_CASES[w];
- if (ACRONYMS.has(w)) return w.toUpperCase();
- return w.charAt(0).toUpperCase() + w.slice(1);
- })
- .join(' ');
-}
-
-function extractHeadings(raw) {
- const stripped = raw
- .replace(/^---[\s\S]*?---\n?/, '')
- .replace(/^(import|export)\s+.*$/gm, '')
- .replace(/```[\s\S]*?```/gm, '');
-
- const headings = [];
- for (const line of stripped.split('\n')) {
- const match = line.match(/^(#{1,2}) (.+)/);
- if (match) headings.push({ level: match[1].length, text: match[2].trim() });
- }
- return headings;
-}
-
-function stripMdxSyntax(raw) {
- return raw
- .replace(/^---[\s\S]*?---\n?/, '') // YAML frontmatter
- .replace(/\{\/\*[\s\S]*?\*\/\}/g, '') // JSX comment blocks: {/* ... */}
- .replace(/^(import|export)\s+.*$/gm, '') // import/export lines
- .replace(/<[A-Z][^/\s>][^>]*\/>/g, '') // self-closing JSX: ,
- .replace(/<\/?[A-Z][^\s>]*[^>]*>/g, '') // JSX open/close: ,
- .replace(/^\s*# .+\n+/, '') // strip leading h1 (redundant with our page header)
- .replace(/^(#{1,5}) /gm, '#$1 ') // shift all headings down one level (## → ###, etc.)
- .replace(/\n{3,}/g, '\n\n') // collapse excess blank lines
- .replace(/\n*---\s*$/, '') // strip trailing hr
- .trim();
-}
-
-const FOLDERS_FIRST = ['intro'];
-const FOLDERS_LAST = ['certs'];
-const FOLDERS_EXCLUDE = ['config'];
-
-const FOLDER_DESCRIPTION_OVERRIDES = {
- contribute: 'How to contribute to the Security Frameworks - either through direct contributions (fixes, new content, enhancements) or by becoming a Framework Steward.',
-};
-const PAGE_DESCRIPTION_OVERRIDES = {
- '/contribute/spotlight-zone': 'The Spotlight Zone is where all contributor activity across the Security Frameworks is tracked and recognized.',
-};
-
-function getFrameworkFolders() {
- const folders = fs
- .readdirSync(PAGES_DIR)
- .filter((entry) => !FOLDERS_EXCLUDE.includes(entry) && fs.statSync(path.join(PAGES_DIR, entry)).isDirectory())
- .sort();
-
- const first = folders.filter((f) => FOLDERS_FIRST.includes(f));
- const last = folders.filter((f) => FOLDERS_LAST.includes(f));
- const rest = folders.filter((f) => !FOLDERS_FIRST.includes(f) && !FOLDERS_LAST.includes(f));
- return [...first, ...rest, ...last];
-}
-
-function getFrameworkDescription(folderName) {
- if (FOLDER_DESCRIPTION_OVERRIDES[folderName]) return FOLDER_DESCRIPTION_OVERRIDES[folderName];
- const folderPath = path.join(PAGES_DIR, folderName);
- for (const candidate of ['overview.mdx', 'introduction.mdx', 'index.mdx']) {
- const candidatePath = path.join(folderPath, candidate);
- if (fs.existsSync(candidatePath)) {
- try {
- const { data } = matter(fs.readFileSync(candidatePath, 'utf-8'));
- if (data.description) return data.description;
- } catch (_) {}
- }
- }
- return '';
-}
-
-// Builds the framework index file: header + AI instructions + overview content + per-page link list
-function buildFrameworkIndex(folderName, title, overviewUrl, frameworkDescription, pages) {
- const lines = [
- `# ${title}`,
- '',
- frameworkDescription ? `> ${frameworkDescription}` : `> Security framework covering ${title.toLowerCase()}.`,
- '',
- `Full documentation: ${overviewUrl}`,
- '',
- '---',
- '',
- '## Instructions for AI Assistants',
- '',
- ...(folderName === 'intro'
- ? [
- 'This file provides orientation about the Security Alliance (SEAL) and describes all available security frameworks. Read it when the user wants a general overview, does not know which framework to look at, or asks about SEAL itself.',
- '',
- '**When responding:**',
- `- Make clear this information comes from the [Security Alliance Frameworks](${BASE_URL}) documentation`,
- `- To answer questions about a specific framework, fetch the relevant file listed at ${BASE_URL}/llms.txt`,
- `- All individual framework files are listed with descriptions at ${BASE_URL}/llms.txt`,
- ]
- : [
- `This is the index for the ${title} framework. The overview is included below for immediate context. For detailed content on a specific topic, fetch the relevant per-page file from the Pages list.`,
- '',
- '**When responding:**',
- `- Reference the [${title} framework](${overviewUrl}) in your answer`,
- '- Fetch a per-page file for detailed content on a specific topic — do not fetch multiple unless explicitly asked',
- `- If the question spans multiple frameworks, check ${BASE_URL}/llms.txt`,
- ]),
- '',
- '---',
- '',
- ];
-
- // Embed the overview (first page) content
- const overview = pages[0];
- if (overview) {
- const sectionHeader = overview.pageTitle !== title ? overview.pageTitle : 'Overview';
- lines.push(`## ${sectionHeader}`);
- lines.push('');
- lines.push(`Source: ${BASE_URL}${overview.urlPath}`);
- lines.push('');
- if (overview.descriptionOverride) lines.push(overview.descriptionOverride + '\n');
- lines.push(overview.strippedContent);
- lines.push('');
- lines.push('---');
- lines.push('');
- }
-
- // List remaining pages with links to their per-page files (first page is already embedded above)
- lines.push('## Pages');
- lines.push('');
- for (const page of pages.slice(1)) {
- const pageFileUrl = `${BASE_URL}/llms/${folderName}/${page.slug}.txt`;
- lines.push(`- [${page.pageTitle}](${pageFileUrl})${page.description ? ` — ${page.description}` : ''}`);
- }
-
- return lines.join('\n');
-}
-
-// Builds a single per-page llms file
-function buildPageFile(folderName, title, overviewUrl, page) {
- const lines = [
- `# ${page.pageTitle}`,
- '',
- ...(page.description ? [`> ${page.description}`, ''] : []),
- `Source: ${BASE_URL}${page.urlPath}`,
- `Framework: [${title}](${overviewUrl})`,
- '',
- '---',
- '',
- ...(page.descriptionOverride ? [page.descriptionOverride, ''] : []),
- page.strippedContent,
- ];
-
- return lines.join('\n');
-}
-
-// Builds the thin routing index (llms.txt)
-function buildRoutingIndex(frameworks) {
- const lines = [
- '# Security Frameworks by SEAL',
- '',
- '> A collection of technology-agnostic security best practices to secure Web3 projects and build resilience against potential threats. Maintained by the Security Alliance (SEAL).',
- '',
- `Full documentation: ${BASE_URL}`,
- '',
- '---',
- '',
- '## Instructions for AI Assistants',
- '',
- 'To help users with a specific topic:',
- '',
- '1. Find the framework that best matches the question in the list below',
- '2. Fetch the framework index file — it includes an overview and links to all per-page files',
- '3. If you need detailed content on a specific topic, fetch the relevant per-page file',
- '4. In your response, name the framework and link to its documentation',
- '',
- 'Do not fetch multiple framework files at once. Each framework index is self-contained.',
- '',
- '---',
- '',
- '## Frameworks',
- '',
- ];
-
- for (const { folderName, title, description, pages } of frameworks) {
- lines.push(`### ${title}`);
- lines.push(`File: ${BASE_URL}/llms/${folderName}.txt`);
- if (description) lines.push(`Description: ${description}`);
- if (pages.length > 0) {
- lines.push(`Topics: ${pages.map((p) => p.pageTitle).join(', ')}`);
- }
- lines.push('');
- }
-
- return lines.join('\n');
-}
-
-const distDir = findDistDir();
-if (!distDir) {
- console.error('Dist directory not found - run docs:build first');
- process.exit(1);
-}
-
-const llmsDir = path.join(distDir, 'llms');
-if (!fs.existsSync(llmsDir)) fs.mkdirSync(llmsDir);
-
-const frameworkFolders = getFrameworkFolders();
-const frameworkMeta = [];
-let totalPageFiles = 0;
-
-for (const folderName of frameworkFolders) {
- const title = toTitleCase(folderName);
- const frameworkDescription = getFrameworkDescription(folderName);
- const sidebarLinks = getSidebarLinksForFolder(folderName);
- const firstLink = sidebarLinks[0];
- const overviewUrl = firstLink ? `${BASE_URL}${firstLink}` : `${BASE_URL}/${folderName}`;
-
- // Collect page data in sidebar order
- const pages = [];
- const seen = new Set();
-
- for (const link of sidebarLinks) {
- const filePath = linkToFilePath(link);
- if (!filePath || seen.has(filePath)) continue;
- seen.add(filePath);
-
- try {
- const raw = fs.readFileSync(filePath, 'utf-8');
- const { data } = matter(raw);
- if (isMainBranch && data.dev === true) continue;
-
- const urlPath = getPageUrl(filePath);
- const pageTitle = stripSiteSuffix(data.title) || path.basename(filePath, path.extname(filePath));
- const slug = link.replace(`/${folderName}/`, '').replace(/\//g, '-');
- const h2headings = extractHeadings(raw).filter((h) => h.level === 2);
- const description = data.description || (h2headings.length > 0 ? h2headings.map((h) => h.text).join(', ') : '');
- const descriptionOverride = PAGE_DESCRIPTION_OVERRIDES[urlPath] || '';
- const strippedContent = stripMdxSyntax(raw);
-
- pages.push({ slug, urlPath, pageTitle, description, descriptionOverride, strippedContent });
- } catch (e) {
- console.error(`Error processing ${link}:`, e.message);
- }
- }
-
- // On main, skip frameworks with no publishable pages (all pages were dev-only)
- if (isMainBranch && pages.length === 0) continue;
-
- // Write per-page files under llms/{folderName}/ — skip the first page (already embedded in the framework index)
- const frameworkLlmsDir = path.join(llmsDir, folderName);
- if (!fs.existsSync(frameworkLlmsDir)) fs.mkdirSync(frameworkLlmsDir);
-
- for (const page of pages.slice(1)) {
- const content = buildPageFile(folderName, title, overviewUrl, page);
- fs.writeFileSync(path.join(frameworkLlmsDir, `${page.slug}.txt`), content);
- totalPageFiles++;
- }
-
- // Write framework index under llms/{folderName}.txt
- const frameworkContent = buildFrameworkIndex(folderName, title, overviewUrl, frameworkDescription, pages);
- fs.writeFileSync(path.join(llmsDir, `${folderName}.txt`), frameworkContent);
-
- frameworkMeta.push({ folderName, title, description: frameworkDescription, pages });
-}
-
-// Write routing index at root
-const routingIndex = buildRoutingIndex(frameworkMeta);
-fs.writeFileSync(path.join(distDir, 'llms.txt'), routingIndex);
-
-console.log(`Done. ${frameworkFolders.length} framework index files + ${totalPageFiles} per-page files + routing index written to ${distDir}`);