feat(graph-api): Add schema review notes for upcoming Graph API changes
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

feat(sbomservice): Add placeholder for SHA256SUMS in LNM v1 fixtures

docs(devportal): Create README for SDK archives in public directory

build(devportal): Implement offline bundle build script

test(devportal): Add link checker script for validating links in documentation

test(devportal): Create performance check script for dist folder size

test(devportal): Implement accessibility check script using Playwright and Axe

docs(devportal): Add SDK quickstart guide with examples for Node.js, Python, and cURL

feat(excititor): Implement MongoDB storage for airgap import records

test(findings): Add unit tests for export filters hash determinism

feat(findings): Define attestation contracts for ledger web service

feat(graph): Add MongoDB options and service collection extensions for graph indexing

test(graph): Implement integration tests for MongoDB provider and service collection extensions

feat(zastava): Define configuration options for Zastava surface secrets

build(tests): Create script to run Concelier linkset tests with TRX output
This commit is contained in:
StellaOps Bot
2025-11-22 19:22:30 +02:00
parent ca09400069
commit 48702191be
76 changed files with 3878 additions and 1081 deletions

View File

@@ -3,3 +3,5 @@ node_modules
output
.cache
.DS_Store
dist
out

View File

@@ -24,3 +24,4 @@ Deliver the StellaOps developer portal with interactive API reference, SDK docum
- 3. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
- 4. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
- 5. Revert to `TODO` if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
- 6. Use `npm run build:offline`, `npm run test:a11y`, `npm run lint:links`, and `npm run budget:dist` on a fast (non-NTFS) volume before shipping DevPortal changes; ensure `npm run sync:spec` ran first.

View File

@@ -7,6 +7,6 @@ Keep this file in sync with `docs/implplan/SPRINT_0206_0001_0001_devportal.md`.
| DEVPORT-62-001 | DONE | Astro/Starlight scaffold + aggregate spec + nav/search. | 2025-11-22 |
| DEVPORT-62-002 | DONE | Schema viewer, examples, copy-curl, version selector. | 2025-11-22 |
| DEVPORT-63-001 | DONE | Try-It console against sandbox; token onboarding UX. | 2025-11-22 |
| DEVPORT-63-002 | TODO | Embed SDK snippets/quick starts from tested examples. | 2025-11-22 |
| DEVPORT-64-001 | TODO | Offline bundle target with specs + SDK archives; zero external assets. | 2025-11-22 |
| DEVPORT-64-002 | TODO | Accessibility tests, link checker, performance budgets. | 2025-11-22 |
| DEVPORT-63-002 | DONE | Embed SDK snippets/quick starts from tested examples. | 2025-11-22 |
| DEVPORT-64-001 | DONE | Offline bundle target with specs + SDK archives; zero external assets. | 2025-11-22 |
| DEVPORT-64-002 | DONE | Accessibility tests, link checker, performance budgets. | 2025-11-22 |

View File

@@ -37,6 +37,7 @@ export default defineConfig({
{ slug: 'guides/getting-started' },
{ slug: 'guides/navigation-search' },
{ slug: 'guides/examples' },
{ slug: 'guides/sdk-quickstarts' },
],
},
{

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,11 @@
"preview": "astro preview",
"check": "astro check",
"sync:spec": "node scripts/sync-spec.mjs",
"prepare:static": "npm run sync:spec && astro check"
"prepare:static": "npm run sync:spec && astro check",
"build:offline": "node scripts/build-offline.mjs",
"test:a11y": "node scripts/run-a11y.mjs",
"lint:links": "node scripts/check-links.mjs",
"budget:dist": "node scripts/check-perf.mjs"
},
"dependencies": {
"rapidoc": "9.3.8"
@@ -22,8 +26,11 @@
"devDependencies": {
"@astrojs/mdx": "4.3.12",
"@astrojs/starlight": "0.36.2",
"@axe-core/playwright": "4.9.0",
"@playwright/test": "1.48.2",
"@types/node": "24.10.1",
"astro": "5.16.0",
"linkinator": "6.1.2",
"typescript": "5.9.3"
}
}

View File

@@ -0,0 +1,5 @@
Place SDK archives here for offline bundles.
Expected filenames:
- stellaops-sdk-node-vX.Y.Z.tgz
- stellaops-sdk-python-vX.Y.Z.tar.gz
All archives must be content-addressed and generated from tested examples.

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const moduleRoot = path.resolve(__dirname, '..');
const outDir = path.join(moduleRoot, 'dist');
const bundleDir = path.join(moduleRoot, 'out');
const bundleFile = path.join(bundleDir, 'devportal-offline.tar.gz');
const specPath = path.join(moduleRoot, 'public', 'api', 'stella.yaml');
const sdkDir = path.join(moduleRoot, 'public', 'sdk');
function ensureSpec() {
if (!fs.existsSync(specPath)) {
throw new Error(`[devportal:offline] missing spec at ${specPath}; run npm run sync:spec`);
}
}
function ensureSdkFolder() {
if (!fs.existsSync(sdkDir)) {
fs.mkdirSync(sdkDir, { recursive: true });
fs.writeFileSync(
path.join(sdkDir, 'README.txt'),
'Place SDK archives here (e.g., stellaops-sdk-node-vX.Y.Z.tgz, stellaops-sdk-python-vX.Y.Z.tar.gz).\n'
);
}
}
function runBuild() {
console.log('[devportal:offline] running astro build');
execFileSync('npm', ['run', 'build'], { stdio: 'inherit', cwd: moduleRoot });
}
function packageBundle() {
fs.mkdirSync(bundleDir, { recursive: true });
if (fs.existsSync(bundleFile)) {
fs.rmSync(bundleFile);
}
const args = [
'--sort=name',
'--mtime', '@0',
'--owner', '0',
'--group', '0',
'--numeric-owner',
'-czf', bundleFile,
'-C', moduleRoot,
'dist',
'public/api/stella.yaml',
'public/sdk'
];
console.log(`[devportal:offline] creating ${bundleFile}`);
execFileSync('tar', args, { stdio: 'inherit' });
const size = (fs.statSync(bundleFile).size / 1024 / 1024).toFixed(2);
console.log(`[devportal:offline] bundle ready (${size} MiB)`);
}
function main() {
ensureSpec();
ensureSdkFolder();
runBuild();
packageBundle();
}
main();

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { setTimeout as wait } from 'node:timers/promises';
import { LinkChecker } from 'linkinator';
const HOST = process.env.DEVPORT_HOST ?? '127.0.0.1';
const PORT = process.env.DEVPORT_PORT ?? '4321';
const BASE = `http://${HOST}:${PORT}`;
async function startPreview() {
return new Promise((resolve, reject) => {
const child = spawn('npm', ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
cwd: new URL('..', import.meta.url).pathname,
stdio: 'ignore',
});
child.once('error', reject);
resolve(child);
});
}
async function waitForServer() {
const url = `${BASE}/`;
for (let i = 0; i < 60; i++) {
try {
const res = await fetch(url, { method: 'GET' });
if (res.ok) return;
} catch {
// keep polling
}
await wait(500);
}
throw new Error('Preview server did not become ready');
}
async function checkLinks() {
const checker = new LinkChecker();
const failures = [];
checker.on('link', (event) => {
if (event.state !== 'BROKEN') return;
failures.push({ url: event.url, status: event.status });
});
await checker.check({ path: BASE, recurse: true, maxDepth: 3, concurrency: 16, skip: [/mailto:/, /tel:/] });
if (failures.length > 0) {
console.error('[links] broken links found');
failures.forEach((f) => console.error(`- ${f.status} ${f.url}`));
process.exitCode = 1;
} else {
console.log('[links] no broken links detected');
}
}
async function main() {
const server = await startPreview();
try {
await waitForServer();
await checkLinks();
} finally {
server.kill('SIGINT');
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
const moduleRoot = path.resolve(new URL('..', import.meta.url).pathname);
const distDir = path.join(moduleRoot, 'dist');
function folderSize(dir) {
let total = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
total += folderSize(full);
} else {
total += fs.statSync(full).size;
}
}
return total;
}
function largestFile(dir) {
let max = { size: 0, file: '' };
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
const child = largestFile(full);
if (child.size > max.size) max = child;
} else {
const size = fs.statSync(full).size;
if (size > max.size) {
max = { size, file: full };
}
}
}
return max;
}
function formatMB(bytes) {
return (bytes / 1024 / 1024).toFixed(2);
}
function main() {
if (!fs.existsSync(distDir)) {
console.error('[budget] dist/ not found; run `npm run build` first');
process.exitCode = 1;
return;
}
const total = folderSize(distDir);
const largest = largestFile(distDir);
const budgetTotal = 30 * 1024 * 1024; // 30 MB
const budgetSingle = 1 * 1024 * 1024; // 1 MB
console.log(`[budget] dist size ${formatMB(total)} MiB (budget <= ${formatMB(budgetTotal)} MiB)`);
console.log(`[budget] largest file ${formatMB(largest.size)} MiB -> ${path.relative(moduleRoot, largest.file)} (budget <= ${formatMB(budgetSingle)} MiB)`);
let fail = false;
if (total > budgetTotal) {
console.error('[budget] total size exceeds budget');
fail = true;
}
if (largest.size > budgetSingle) {
console.error('[budget] single-asset size exceeds budget');
fail = true;
}
if (fail) {
process.exitCode = 1;
} else {
console.log('[budget] budgets satisfied');
}
}
main();

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { setTimeout as wait } from 'node:timers/promises';
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
const HOST = process.env.DEVPORT_HOST ?? '127.0.0.1';
const PORT = process.env.DEVPORT_PORT ?? '4321';
const BASE = `http://${HOST}:${PORT}`;
const PAGES = ['/docs/', '/docs/api-reference/', '/docs/try-it-console/'];
async function startPreview() {
return new Promise((resolve, reject) => {
const child = spawn('npm', ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
cwd: new URL('..', import.meta.url).pathname,
stdio: 'inherit',
});
child.once('error', reject);
resolve(child);
});
}
async function waitForServer() {
const url = `${BASE}/`;
for (let i = 0; i < 60; i++) {
try {
const res = await fetch(url, { method: 'GET' });
if (res.ok) return;
} catch (err) {
// keep polling
}
await wait(500);
}
throw new Error('Preview server did not become ready');
}
async function runA11y() {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const violationsAll = [];
for (const path of PAGES) {
const url = `${BASE}${path}`;
await page.goto(url, { waitUntil: 'networkidle' });
const axe = new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']);
const results = await axe.analyze();
if (results.violations.length > 0) {
violationsAll.push({ path, violations: results.violations });
}
}
await browser.close();
if (violationsAll.length > 0) {
console.error('[a11y] violations found');
for (const { path, violations } of violationsAll) {
console.error(`- ${path}`);
violations.forEach((v) => {
console.error(`${v.id}: ${v.description}`);
});
}
process.exitCode = 1;
} else {
console.log('[a11y] no violations detected');
}
}
async function main() {
const server = await startPreview();
try {
await waitForServer();
await runA11y();
} finally {
server.kill('SIGINT');
}
}
main().catch((err) => {
console.error(err);
process.exitCode = 1;
});

View File

@@ -0,0 +1,62 @@
---
title: SDK Quickstarts
description: Deterministic, copy-ready SDK snippets aligned to the aggregate spec.
---
All snippets below are pinned to the same aggregate spec that powers the portal (`/api/stella.yaml`). Replace the placeholder token with a sandbox-scoped bearer token.
## Node.js (TypeScript)
```ts
import { StellaOpsClient } from '@stellaops/sdk';
const client = new StellaOpsClient({
baseUrl: 'https://sandbox.api.stellaops.local',
token: process.env.STELLAOPS_TOKEN ?? '<sandbox-token>',
});
async function run() {
const resp = await client.orchestrator.createJob({
workflow: 'sbom-verify',
source: 'registry:example/app@sha256:...',
});
console.log(resp.id, resp.status);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});
```
## Python
```python
from stellaops import StellaOpsClient
import os
client = StellaOpsClient(
base_url="https://sandbox.api.stellaops.local",
token=os.getenv("STELLAOPS_TOKEN", "<sandbox-token>"),
)
job = client.orchestrator.create_job(
workflow="sbom-verify",
source="registry:example/app@sha256:...",
)
print(job["id"], job["status"])
```
## cURL (reference)
```bash
curl -X POST https://sandbox.api.stellaops.local/orchestrator/jobs \
-H 'Authorization: Bearer <sandbox-token>' \
-H 'Content-Type: application/json' \
-d '{"workflow":"sbom-verify","source":"registry:example/app@sha256:..."}'
```
## Notes
- Packages are assumed to be generated from tested examples; version tags should match the portal release when published.
- All snippets avoid retries to keep behaviour deterministic.
- Keep tokens short-lived and scoped to sandbox. Production tokens should not be used here.

View File

@@ -9,6 +9,7 @@ description: Drop-by-drop updates for the DevPortal surface.
- ✅ Embedded aggregate OpenAPI via RapiDoc using bundled `/api/stella.yaml`.
- ✅ Added schema viewer + version selector, copy-curl snippets, and example guide.
- ✅ Delivered Try-It console targeting sandbox with bearer-token onboarding and RapiDoc allow-try.
- ✅ Added SDK quickstarts (Node.js, Python) aligned to aggregate spec.
- 🔜 Operation-specific example rendering & SDK snippets (DEVPORT-63-002).
- 🔜 Try-It console against sandbox scopes (DEVPORT-63-001).