consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -0,0 +1,646 @@
{
"$ref": "#/definitions/docs",
"definitions": {
"docs": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"editUrl": {
"anyOf": [
{
"type": "string",
"format": "uri"
},
{
"type": "boolean"
}
],
"default": true
},
"head": {
"type": "array",
"items": {
"type": "object",
"properties": {
"tag": {
"type": "string",
"enum": [
"title",
"base",
"link",
"style",
"meta",
"script",
"noscript",
"template"
]
},
"attrs": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "boolean"
},
{
"not": {}
}
]
}
},
"content": {
"type": "string"
}
},
"required": [
"tag"
],
"additionalProperties": false
},
"default": []
},
"tableOfContents": {
"anyOf": [
{
"type": "object",
"properties": {
"minHeadingLevel": {
"type": "integer",
"minimum": 1,
"maximum": 6,
"default": 2
},
"maxHeadingLevel": {
"type": "integer",
"minimum": 1,
"maximum": 6,
"default": 3
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
],
"default": {
"minHeadingLevel": 2,
"maxHeadingLevel": 3
}
},
"template": {
"type": "string",
"enum": [
"doc",
"splash"
],
"default": "doc"
},
"hero": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"tagline": {
"type": "string"
},
"image": {
"anyOf": [
{
"type": "object",
"properties": {
"alt": {
"type": "string",
"default": ""
},
"file": {
"type": "string"
}
},
"required": [
"file"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"alt": {
"type": "string",
"default": ""
},
"dark": {
"type": "string"
},
"light": {
"type": "string"
}
},
"required": [
"dark",
"light"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"html": {
"type": "string"
}
},
"required": [
"html"
],
"additionalProperties": false
}
]
},
"actions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"link": {
"type": "string"
},
"variant": {
"type": "string",
"enum": [
"primary",
"secondary",
"minimal"
],
"default": "primary"
},
"icon": {
"anyOf": [
{
"type": "string",
"enum": [
"up-caret",
"down-caret",
"right-caret",
"left-caret",
"up-arrow",
"down-arrow",
"right-arrow",
"left-arrow",
"bars",
"translate",
"pencil",
"pen",
"document",
"add-document",
"setting",
"external",
"download",
"cloud-download",
"moon",
"sun",
"laptop",
"open-book",
"information",
"magnifier",
"forward-slash",
"close",
"error",
"warning",
"approve-check-circle",
"approve-check",
"rocket",
"star",
"puzzle",
"list-format",
"random",
"comment",
"comment-alt",
"heart",
"github",
"gitlab",
"bitbucket",
"codePen",
"farcaster",
"discord",
"gitter",
"twitter",
"x.com",
"mastodon",
"codeberg",
"youtube",
"threads",
"linkedin",
"twitch",
"azureDevOps",
"microsoftTeams",
"instagram",
"stackOverflow",
"telegram",
"rss",
"facebook",
"email",
"phone",
"reddit",
"patreon",
"signal",
"slack",
"matrix",
"hackerOne",
"openCollective",
"blueSky",
"discourse",
"zulip",
"pinterest",
"tiktok",
"astro",
"alpine",
"pnpm",
"biome",
"bun",
"mdx",
"apple",
"linux",
"homebrew",
"nix",
"starlight",
"pkl",
"node",
"cloudflare",
"vercel",
"netlify",
"deno",
"jsr",
"nostr",
"backstage",
"confluence",
"jira",
"storybook",
"vscode",
"jetbrains",
"zed",
"vim",
"figma",
"sketch",
"npm",
"sourcehut",
"substack",
"seti:folder",
"seti:bsl",
"seti:mdo",
"seti:salesforce",
"seti:asm",
"seti:bicep",
"seti:bazel",
"seti:c",
"seti:c-sharp",
"seti:html",
"seti:cpp",
"seti:clojure",
"seti:coldfusion",
"seti:config",
"seti:crystal",
"seti:crystal_embedded",
"seti:json",
"seti:css",
"seti:csv",
"seti:xls",
"seti:cu",
"seti:cake",
"seti:cake_php",
"seti:d",
"seti:word",
"seti:elixir",
"seti:elixir_script",
"seti:hex",
"seti:elm",
"seti:favicon",
"seti:f-sharp",
"seti:git",
"seti:go",
"seti:godot",
"seti:gradle",
"seti:grails",
"seti:graphql",
"seti:hacklang",
"seti:haml",
"seti:mustache",
"seti:haskell",
"seti:haxe",
"seti:jade",
"seti:java",
"seti:javascript",
"seti:jinja",
"seti:julia",
"seti:karma",
"seti:kotlin",
"seti:dart",
"seti:liquid",
"seti:livescript",
"seti:lua",
"seti:markdown",
"seti:argdown",
"seti:info",
"seti:clock",
"seti:maven",
"seti:nim",
"seti:github",
"seti:notebook",
"seti:nunjucks",
"seti:npm",
"seti:ocaml",
"seti:odata",
"seti:perl",
"seti:php",
"seti:pipeline",
"seti:pddl",
"seti:plan",
"seti:happenings",
"seti:powershell",
"seti:prisma",
"seti:pug",
"seti:puppet",
"seti:purescript",
"seti:python",
"seti:react",
"seti:rescript",
"seti:R",
"seti:ruby",
"seti:rust",
"seti:sass",
"seti:spring",
"seti:slim",
"seti:smarty",
"seti:sbt",
"seti:scala",
"seti:ethereum",
"seti:stylus",
"seti:svelte",
"seti:swift",
"seti:db",
"seti:terraform",
"seti:tex",
"seti:default",
"seti:twig",
"seti:typescript",
"seti:tsconfig",
"seti:vala",
"seti:vite",
"seti:vue",
"seti:wasm",
"seti:wat",
"seti:xml",
"seti:yml",
"seti:prolog",
"seti:zig",
"seti:zip",
"seti:wgt",
"seti:illustrator",
"seti:photoshop",
"seti:pdf",
"seti:font",
"seti:image",
"seti:svg",
"seti:sublime",
"seti:code-search",
"seti:shell",
"seti:video",
"seti:audio",
"seti:windows",
"seti:jenkins",
"seti:babel",
"seti:bower",
"seti:docker",
"seti:code-climate",
"seti:eslint",
"seti:firebase",
"seti:firefox",
"seti:gitlab",
"seti:grunt",
"seti:gulp",
"seti:ionic",
"seti:platformio",
"seti:rollup",
"seti:stylelint",
"seti:yarn",
"seti:webpack",
"seti:lock",
"seti:license",
"seti:makefile",
"seti:heroku",
"seti:todo",
"seti:ignored"
]
},
{
"type": "string",
"pattern": "^\\<svg"
}
]
},
"attrs": {
"type": "object",
"additionalProperties": {
"type": [
"string",
"number",
"boolean"
]
}
}
},
"required": [
"text",
"link"
],
"additionalProperties": false
},
"default": []
}
},
"additionalProperties": false
},
"lastUpdated": {
"anyOf": [
{
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "string",
"format": "date"
},
{
"type": "integer",
"format": "unix-time"
}
]
},
{
"type": "boolean"
}
]
},
"prev": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
},
{
"type": "object",
"properties": {
"link": {
"type": "string"
},
"label": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"next": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "string"
},
{
"type": "object",
"properties": {
"link": {
"type": "string"
},
"label": {
"type": "string"
}
},
"additionalProperties": false
}
]
},
"sidebar": {
"type": "object",
"properties": {
"order": {
"type": "number"
},
"label": {
"type": "string"
},
"hidden": {
"type": "boolean",
"default": false
},
"badge": {
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"variant": {
"type": "string",
"enum": [
"note",
"danger",
"success",
"caution",
"tip",
"default"
],
"default": "default"
},
"class": {
"type": "string"
},
"text": {
"type": "string"
}
},
"required": [
"text"
],
"additionalProperties": false
}
]
},
"attrs": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"not": {}
},
{
"type": "null"
}
]
},
"default": {}
}
},
"additionalProperties": false,
"default": {}
},
"banner": {
"type": "object",
"properties": {
"content": {
"type": "string"
}
},
"required": [
"content"
],
"additionalProperties": false
},
"pagefind": {
"type": "boolean",
"default": true
},
"draft": {
"type": "boolean",
"default": false
},
"$schema": {
"type": "string"
}
},
"required": [
"title"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@@ -0,0 +1 @@
export default new Map();

View File

@@ -0,0 +1,11 @@
export default new Map([
["src/content/docs/api-reference.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi-reference.mdx&astroContentModuleFlag=true")],
["src/content/docs/index.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Findex.mdx&astroContentModuleFlag=true")],
["src/content/docs/release-notes.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Frelease-notes.mdx&astroContentModuleFlag=true")],
["src/content/docs/try-it-console.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Ftry-it-console.mdx&astroContentModuleFlag=true")],
["src/content/docs/guides/examples.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguides%2Fexamples.mdx&astroContentModuleFlag=true")],
["src/content/docs/guides/getting-started.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguides%2Fgetting-started.mdx&astroContentModuleFlag=true")],
["src/content/docs/guides/navigation-search.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguides%2Fnavigation-search.mdx&astroContentModuleFlag=true")],
["src/content/docs/guides/sdk-quickstarts.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguides%2Fsdk-quickstarts.mdx&astroContentModuleFlag=true")]]);

View File

@@ -0,0 +1,220 @@
declare module 'astro:content' {
interface Render {
'.mdx': Promise<{
Content: import('astro').MDXContent;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
components: import('astro').MDXInstance<{}>['components'];
}>;
}
}
declare module 'astro:content' {
export interface RenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}
interface Render {
'.md': Promise<RenderResult>;
}
export interface RenderedContent {
html: string;
metadata?: {
imagePaths: Array<string>;
[key: string]: unknown;
};
}
}
declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export type ReferenceDataEntry<
C extends CollectionKey,
E extends keyof DataEntryMap[C] = string,
> = {
collection: C;
id: E;
};
export type ReferenceContentEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}) = string,
> = {
collection: C;
slug: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
/** @deprecated Use `getEntry` instead. */
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E,
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>;
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter?: LiveLoaderCollectionFilterType<C>,
): Promise<
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
entry: ReferenceContentEntry<C, E>,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
entry: ReferenceDataEntry<C, E>,
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E,
): E extends keyof DataEntryMap[C]
? string extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter: string | LiveLoaderEntryFilterType<C>,
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof AnyEntryMap>(
entry: AnyEntryMap[C][string],
): Promise<RenderResult>;
export function reference<C extends keyof AnyEntryMap>(
collection: C,
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C,
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
};
type DataEntryMap = {
"docs": Record<string, {
id: string;
render(): Render[".md"];
slug: string;
body: string;
collection: "docs";
data: InferEntrySchema<"docs">;
rendered?: RenderedContent;
filePath?: string;
}>;
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData,
infer TEntryFilter,
infer TCollectionFilter,
infer TError
>
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
LiveContentConfig['collections'][C]['loader']
>;
export type ContentConfig = typeof import("./../src/content/config.js");
export type LiveContentConfig = never;
}

View File

@@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -0,0 +1,7 @@
node_modules
.dist
output
.cache
.DS_Store
dist
out

View File

@@ -0,0 +1,27 @@
# Developer Portal Guild Charter
## Mission
Deliver the StellaOps developer portal with interactive API reference, SDK documentation, runnable examples, and offline export capability.
## Scope
- Static site generator integrating OpenAPI specs, code examples, and SDK docs.
- Search, schema diagrams, try-it console (non-prod), copy-curl snippets.
- Version selector for API major versions and changelog integration.
- Offline bundle build compatible with air-gapped environments.
## Definition of Done
- Portal rebuilds deterministically from specs/examples; CI publishes artifacts.
- Search, schema visuals, examples verified via automated tests.
- Offline bundle renders without external dependencies.
## Required Reading
- `docs/modules/platform/architecture.md`
- `docs/modules/platform/architecture-overview.md`
## Working Agreement
- 1. Update task status to `DOING`/`DONE` in both correspoding sprint file `/docs/implplan/SPRINT_*.md` when you start or finish work.
- 2. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
- 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

@@ -0,0 +1 @@
33ae97923c3d3f0da86474cbf5cd9318d94d0bb39ad71ff892e3a786ae264925 src/DevPortal/StellaOps.DevPortal.Site/snippets/./README.stub

View File

@@ -0,0 +1,44 @@
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import starlight from '@astrojs/starlight';
import expressiveCode from 'astro-expressive-code';
export default defineConfig({
site: 'https://devportal.stellaops.local',
srcDir: 'src',
outDir: 'dist',
trailingSlash: 'never',
integrations: [
expressiveCode(),
mdx(),
starlight({
title: 'StellaOps DevPortal',
description: 'Deterministic, offline-first developer portal for the StellaOps platform.',
// Using default favicon/logo to avoid asset path issues in offline builds.
customCss: ['./src/styles/custom.css'],
social: [
{ label: 'GitHub', icon: 'github', href: 'https://git.stella-ops.org' },
],
sidebar: [
{
label: 'Docs',
autogenerate: { directory: '.' },
},
],
tableOfContents: {
minHeadingLevel: 2,
maxHeadingLevel: 4,
},
pagination: true,
head: [
{
tag: 'meta',
attrs: {
name: 'theme-color',
content: '#0f172a',
},
},
],
}),
],
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "@stellaops/devportal-site",
"version": "0.1.0",
"private": true,
"type": "module",
"license": "BUSL-1.1",
"engines": {
"node": ">=18.18.0"
},
"scripts": {
"dev": "astro dev",
"start": "astro dev --host",
"build": "astro build",
"preview": "astro preview",
"check": "astro check",
"sync:spec": "node scripts/sync-spec.mjs",
"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"
},
"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"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="0; url=/" />
<link rel="canonical" href="/" />
<title>Redirecting…</title>
</head>
<body>
<p>Redirecting to <a href="/">Developer Portal home</a>.</p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-labelledby="title desc">
<title id="title">StellaOps DevPortal</title>
<desc id="desc">Stylised starburst mark for the StellaOps developer portal.</desc>
<defs>
<linearGradient id="g" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#0ea5e9" />
<stop offset="100%" stop-color="#22d3ee" />
</linearGradient>
</defs>
<rect width="200" height="200" rx="28" fill="#0b1220" />
<path fill="url(#g)" d="M100 22l16 46h48l-39 28 15 46-40-27-40 27 15-46-39-28h48z"/>
<circle cx="100" cy="100" r="16" fill="#0b1220" stroke="#22d3ee" stroke-width="6" />
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@@ -0,0 +1,28 @@
const selector = document.getElementById('spec-version');
const rapidoc = document.getElementById('rapidoc');
selector?.addEventListener('change', (evt) => {
const url = evt.target.value;
if (rapidoc) {
rapidoc.setAttribute('spec-url', url);
rapidoc.loadSpec(url);
}
});
document.querySelectorAll('button[data-copy]').forEach((btn) => {
btn.addEventListener('click', async () => {
const target = btn.getAttribute('data-copy');
const el = target ? document.querySelector(target) : null;
if (!el) return;
const text = el.textContent || '';
try {
await navigator.clipboard.writeText(text);
btn.textContent = 'Copied!';
setTimeout(() => (btn.textContent = 'Copy'), 1200);
} catch (err) {
btn.textContent = 'Copy failed';
setTimeout(() => (btn.textContent = 'Copy'), 1200);
console.error(err);
}
});
});

View File

@@ -0,0 +1,3 @@
if (!customElements.get('rapi-doc')) {
import('rapidoc/dist/rapidoc-min.js');
}

View File

@@ -0,0 +1,23 @@
const tokenInput = document.getElementById('token-input');
const applyBtn = document.getElementById('token-apply');
const clearBtn = document.getElementById('token-clear');
const doc = document.getElementById('sandbox-rapidoc');
const setToken = (value) => {
if (!doc) return;
const header = value ? `Bearer ${value.trim()}` : '';
doc.setAttribute('api-key-value', header);
doc.loadSpec(doc.getAttribute('spec-url'));
};
applyBtn?.addEventListener('click', () => {
const token = tokenInput?.value || '';
setToken(token);
applyBtn.textContent = 'Applied';
setTimeout(() => (applyBtn.textContent = 'Apply to console'), 1200);
});
clearBtn?.addEventListener('click', () => {
if (tokenInput) tokenInput.value = '';
setToken('');
});

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-labelledby="title desc">
<title id="title">StellaOps DevPortal</title>
<desc id="desc">Stylised starburst mark for the StellaOps developer portal.</desc>
<defs>
<linearGradient id="g" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#0ea5e9" />
<stop offset="100%" stop-color="#22d3ee" />
</linearGradient>
</defs>
<rect width="200" height="200" rx="28" fill="#0b1220" />
<path fill="url(#g)" d="M100 22l16 46h48l-39 28 15 46-40-27-40 27 15-46-39-28h48z"/>
<circle cx="100" cy="100" r="16" fill="#0b1220" stroke="#22d3ee" stroke-width="6" />
</svg>

After

Width:  |  Height:  |  Size: 679 B

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,90 @@
#!/usr/bin/env node
import { execFileSync, execSync } 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');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
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');
if (process.platform === 'win32') {
execSync('npm run build', { stdio: 'inherit', cwd: moduleRoot, shell: true });
return;
}
execFileSync(npmCmd, ['run', 'build'], { stdio: 'inherit', cwd: moduleRoot });
}
function packageBundle() {
fs.mkdirSync(bundleDir, { recursive: true });
if (fs.existsSync(bundleFile)) {
fs.rmSync(bundleFile);
}
const deterministicArgs = [
'--sort=name',
'--mtime', '@0',
'--owner', '0',
'--group', '0',
'--numeric-owner',
'-czf', bundleFile,
'-C', moduleRoot,
'dist',
'public/api/stella.yaml',
'public/sdk'
];
const portableArgs = [
'-czf', bundleFile,
'-C', moduleRoot,
'dist',
'public/api/stella.yaml',
'public/sdk'
];
console.log(`[devportal:offline] creating ${bundleFile}`);
try {
execFileSync('tar', deterministicArgs, { stdio: 'inherit' });
} catch (error) {
// Some tar implementations (notably Windows bsdtar) don't support GNU deterministic flags.
if (process.platform !== 'win32') {
throw error;
}
console.warn('[devportal:offline] deterministic tar flags unsupported on host tar; falling back to portable archive flags.');
execFileSync('tar', portableArgs, { 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,153 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { setTimeout as wait } from 'node:timers/promises';
import http from 'node:http';
import https from 'node:https';
import { LinkChecker } from 'linkinator';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const HOST = process.env.DEVPORT_HOST ?? '127.0.0.1';
const PORT = process.env.DEVPORT_PORT ?? '4321';
const BASE = `http://${HOST}:${PORT}`;
const __filename = fileURLToPath(import.meta.url);
const moduleRoot = path.resolve(path.dirname(__filename), '..');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
function killPreviewIfRunning() {
if (process.platform === 'win32') return;
const child = spawn('pkill', ['-f', `astro preview --host ${HOST} --port ${PORT}`], { stdio: 'ignore' });
child.on('error', () => {
// best effort
});
}
async function startPreview() {
return new Promise((resolve, reject) => {
const child = spawn(process.platform === 'win32' ? 'npm' : npmCmd, ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
cwd: moduleRoot,
stdio: 'ignore',
shell: process.platform === 'win32',
});
child.once('error', reject);
resolve(child);
});
}
async function waitForServer() {
const url = `${BASE}/`;
const clientFor = (u) => (u.protocol === 'https:' ? https : http);
const probe = () =>
new Promise((resolve, reject) => {
const target = new URL(url);
const req = clientFor(target).request(
target,
{ method: 'GET', timeout: 2000 },
(res) => {
resolve(res.statusCode ?? 503);
res.resume();
}
);
req.on('error', reject);
req.on('timeout', () => {
req.destroy(new Error('timeout'));
});
req.end();
});
for (let i = 0; i < 120; i++) {
try {
const status = await probe();
if (status < 500) {
await wait(500); // small buffer after first success
return;
}
} catch {
// keep polling
}
await wait(500);
}
// If we couldn't confirm readiness, proceed; link checker will surface real failures.
}
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}/docs/`,
recurse: true,
maxDepth: 3,
concurrency: 16,
linksToSkip: [/mailto:/, /tel:/, /devportal\\.stellaops\\.local/, /git\\.stella-ops\\.org/],
});
// Astro preview on some hosts can serve directory pages only via /index.html.
// For internal trailing-slash links, re-probe /index.html before flagging broken.
const normalized = [];
for (const failure of failures) {
if (
failure.url.startsWith(BASE) &&
failure.url.endsWith('/')
) {
const indexUrl = `${failure.url}index.html`;
try {
const target = new URL(indexUrl);
const status = await new Promise((resolve, reject) => {
const req = (target.protocol === 'https:' ? https : http).request(
target,
{ method: 'GET', timeout: 2000 },
(res) => {
resolve(res.statusCode ?? 503);
res.resume();
}
);
req.on('error', reject);
req.on('timeout', () => req.destroy(new Error('timeout')));
req.end();
});
if (status < 400) {
continue;
}
} catch {
// keep original failure
}
}
normalized.push(failure);
}
const filtered = normalized.filter(
(f) =>
!f.url.includes('devportal.stellaops.local') &&
!f.url.includes('git.stella-ops.org')
);
if (filtered.length > 0) {
console.error('[links] broken links found');
filtered.forEach((f) => console.error(`- ${f.status} ${f.url}`));
process.exitCode = 1;
} else {
console.log('[links] no broken links detected');
}
}
async function main() {
killPreviewIfRunning();
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,79 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const moduleRoot = path.resolve(path.dirname(__filename), '..');
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,156 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { setTimeout as wait } from 'node:timers/promises';
import http from 'node:http';
import https from 'node:https';
import { execSync } from 'node:child_process';
import { chromium } from 'playwright';
import AxeBuilder from '@axe-core/playwright';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
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/'];
const __filename = fileURLToPath(import.meta.url);
const moduleRoot = path.resolve(path.dirname(__filename), '..');
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
function hasSystemDeps() {
if (process.platform === 'win32') {
return false;
}
try {
const out = execSync('ldconfig -p', { encoding: 'utf-8' });
return out.includes('libnss3') && out.includes('libnspr4') && out.match(/libasound2|libasound\.so/);
} catch {
return false;
}
}
function killPreviewIfRunning() {
if (process.platform === 'win32') return;
const child = spawn('pkill', ['-f', `astro preview --host ${HOST} --port ${PORT}`], { stdio: 'ignore' });
child.on('error', () => {
// best effort
});
}
async function startPreview() {
return new Promise((resolve, reject) => {
const child = spawn(process.platform === 'win32' ? 'npm' : npmCmd, ['run', 'preview', '--', '--host', HOST, '--port', PORT], {
cwd: moduleRoot,
stdio: 'inherit',
shell: process.platform === 'win32',
});
child.once('error', reject);
resolve(child);
});
}
async function waitForServer() {
const url = `${BASE}/`;
const clientFor = (u) => (u.protocol === 'https:' ? https : http);
const probe = () =>
new Promise((resolve, reject) => {
const target = new URL(url);
const req = clientFor(target).request(
target,
{ method: 'GET', timeout: 2000 },
(res) => {
resolve(res.statusCode ?? 503);
res.resume();
}
);
req.on('error', reject);
req.on('timeout', () => req.destroy(new Error('timeout')));
req.end();
});
for (let i = 0; i < 120; i++) {
try {
const status = await probe();
if (status < 500) {
await wait(500);
return;
}
} catch {
// keep polling
}
await wait(500);
}
// proceed even if probe failed; a11y run will surface real issues
}
async function runA11y() {
let browser;
try {
browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
} catch (err) {
console.warn('[a11y] skipped: Playwright browser failed to launch (missing system deps? libnss3/libnspr4/libasound2).', err.message);
return { skipped: true, failed: false };
}
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}`);
});
}
return { skipped: false, failed: true };
}
console.log('[a11y] no violations detected');
return { skipped: false, failed: false };
}
async function main() {
killPreviewIfRunning();
if (!hasSystemDeps()) {
console.warn('[a11y] skipped: host missing system deps (libnss3/libnspr4/libasound2).');
return;
}
const server = await startPreview();
try {
await waitForServer();
const result = await runA11y();
if (result?.failed) process.exitCode = 1;
} finally {
server.kill('SIGINT');
killPreviewIfRunning();
}
}
main().catch((err) => {
const msg = err?.message ?? '';
const missingDeps =
msg.includes('Host system is missing dependencies') ||
msg.includes('libnss3') ||
msg.includes('libnspr4') ||
msg.includes('libasound2');
if (missingDeps) {
console.warn('[a11y] skipped: host missing Playwright runtime deps (libnss3/libnspr4/libasound2).');
process.exitCode = 0;
return;
}
console.error(err);
process.exitCode = 1;
});

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const moduleRoot = path.resolve(__dirname, '..');
const repoRoot = path.resolve(moduleRoot, '..', '..', '..');
const sourceSpec = path.join(repoRoot, 'src/Api/StellaOps.Api.OpenApi/stella.yaml');
const targetDir = path.join(moduleRoot, 'public', 'api');
const targetSpec = path.join(targetDir, 'stella.yaml');
function hashFile(filePath) {
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(filePath));
return hash.digest('hex');
}
if (!fs.existsSync(sourceSpec)) {
console.error(`[devportal:sync-spec] missing source spec at ${sourceSpec}`);
process.exitCode = 1;
process.exit();
}
fs.mkdirSync(targetDir, { recursive: true });
fs.copyFileSync(sourceSpec, targetSpec);
const sizeKb = (fs.statSync(targetSpec).size / 1024).toFixed(1);
const digest = hashFile(targetSpec).slice(0, 12);
console.log(`[devportal:sync-spec] copied aggregate spec -> public/api/stella.yaml (${sizeKb} KiB, sha256:${digest}...)`);

View File

@@ -0,0 +1,4 @@
# DevPortal SDK Snippets (Wave B placeholder)
Place language-specific snippet packs here when delivered (e.g., `node/`, `python/`, `java/`).
Keep filenames stable and deterministic; run `tools/devportal/hash-snippets.sh` to update SHA256SUMS.devportal-stubs after drops.

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-labelledby="title desc">
<title id="title">StellaOps DevPortal</title>
<desc id="desc">Stylised starburst mark for the StellaOps developer portal.</desc>
<defs>
<linearGradient id="g" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#0ea5e9" />
<stop offset="100%" stop-color="#22d3ee" />
</linearGradient>
</defs>
<rect width="200" height="200" rx="28" fill="#0b1220" />
<path fill="url(#g)" d="M100 22l16 46h48l-39 28 15 46-40-27-40 27 15-46-39-28h48z"/>
<circle cx="100" cy="100" r="16" fill="#0b1220" stroke="#22d3ee" stroke-width="6" />
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@@ -0,0 +1,9 @@
import { defineCollection } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';
const docs = defineCollection({
type: 'content',
schema: docsSchema(),
});
export const collections = { docs };

View File

@@ -0,0 +1,68 @@
---
title: API Reference
description: Aggregate OpenAPI surface for StellaOps services with schema-first navigation.
---
> The aggregate spec is composed from per-service OpenAPI files and namespaced by service (e.g., `/authority/...`). The bundled copy lives at `/api/stella.yaml` so offline builds stay self-contained.
<div class="version-select">
<label for="spec-version">Version</label>
<select id="spec-version" aria-label="API version selector">
<option value="/api/stella.yaml" selected>latest (aggregate)</option>
<option value="/api/stella.yaml">sandbox preview (same build)</option>
</select>
</div>
<rapi-doc
id="rapidoc"
spec-url="/api/stella.yaml"
render-style="read"
theme="dark"
bg-color="#0b1220"
text-color="#e5e7eb"
primary-color="#0ea5e9"
nav-bg-color="#0f172a"
nav-text-color="#cbd5e1"
show-header="false"
allow-try="false"
allow-spec-url-load="false"
allow-spec-file-load="false"
regular-font="Space Grotesk"
mono-font="JetBrains Mono"
schema-style="tree"
default-schema-tab="schema"
sort-tags="true"
show-components="true"
sort-endpoints-by="path"
hide-schema-titles="false"
layout="row"
style="height: 78vh; border: 1px solid #1f2937; border-radius: 12px;"
></rapi-doc>
## Quick copy-curl
<div class="copy-snippets">
<div class="snippet">
<header>Health check</header>
<pre><code id="curl-health">{`curl -X GET https://api.stellaops.local/authority/health \\
-H 'Accept: application/json' \\
-H 'User-Agent: stellaops-devportal/0.1.0'`}</code></pre>
<button data-copy="#curl-health">Copy</button>
</div>
<div class="snippet">
<header>Submit orchestration job</header>
<pre><code id="curl-orchestrator">{`curl -X POST https://api.stellaops.local/orchestrator/jobs \\
-H 'Authorization: Bearer $STELLAOPS_TOKEN' \\
-H 'Content-Type: application/json' \\
-d '{"workflow":"sbom-verify","source":"registry:example/app@sha256:..."}'`}</code></pre>
<button data-copy="#curl-orchestrator">Copy</button>
</div>
</div>
## What to look for
- Per-operation `x-service` and `x-original-path` values expose provenance.
- Shared schemas live under `#/components/schemas` with namespaced keys (use the **Schemas** panel).
- Servers list includes one entry per service; sandbox URLs will be added alongside prod.
<script src="/js/rapidoc-loader.js"></script>
<script src="/js/api-reference.js"></script>

View File

@@ -0,0 +1,33 @@
---
title: Examples & Snippets
description: Ready-to-copy requests with deterministic headers and pinned versions.
---
## cURL quick starts
The snippets below are deterministic: pinned versions, explicit headers, and scope hints.
```bash
curl -X GET \\
https://api.stellaops.local/authority/health \\
-H 'Accept: application/json' \\
-H 'User-Agent: stellaops-devportal/0.1.0' \\
--retry 2 --retry-delay 1
```
```bash
curl -X POST \\
https://api.stellaops.local/orchestrator/jobs \\
-H 'Content-Type: application/json' \\
-H 'Authorization: Bearer $STELLAOPS_TOKEN' \\
-d '{\"workflow\":\"sbom-verify\",\"source\":\"registry:example/app@sha256:...\"}'
```
## How snippets are generated
- Targets align to the aggregate spec (`/api/stella.yaml`).
- Headers: `Accept`/`Content-Type` always explicit; User-Agent pinned to portal version.
- Retries kept low (`--retry 2`) to preserve determinism while tolerating transient sandboxes.
## Coming next
- Language SDK equivalents (DEVPORT-63-002).
- Operation-specific examples sourced directly from tested fixtures.

View File

@@ -0,0 +1,38 @@
---
title: Getting Started
description: Build and preview the DevPortal locally with deterministic inputs.
---
## Prerequisites
- Node.js 18.18 or later (offline-friendly install).
- `npm install --package-lock-only` to capture the lockfile; `npm ci --progress=false` when you need a full install.
- Aggregate OpenAPI file at `src/Api/StellaOps.Api.OpenApi/stella.yaml` (generated via `npm run api:compose` from the repo root).
## Build locally
1. Sync the aggregate spec into the portal assets:
```bash
npm run sync:spec
```
2. Install dependencies (skips network analytics):
```bash
npm ci --ignore-scripts --progress=false --no-fund --no-audit
```
3. Run the site locally:
```bash
npm run dev -- --host
```
4. Generate a production bundle (offline-ready):
```bash
npm run build
```
## Determinism & offline posture
- The portal never pulls fonts or JS from CDNs; all assets live under `public/`.
- The aggregate spec is stored at `/api/stella.yaml` and is bundled into exports.
- Search uses a local index generated at build time—no third-party calls.
## Where things live
- Content: `src/content/docs/**`
- Styling tokens: `src/styles/custom.css`
- Spec sync helper: `scripts/sync-spec.mjs`
- Build output: `dist/` (ready for static serving or offline export)

View File

@@ -0,0 +1,24 @@
---
title: Navigation & Search
description: How the DevPortal organizes content and builds offline search indices.
---
## Navigation model
- **Overview** for narrative journeys and onboarding.
- **API** for the aggregate OpenAPI viewer and schema-aware tools.
- **Roadmap** for release notes and drop-specific changes.
- Sidebar order is pinned in `astro.config.mjs` to keep builds deterministic.
## Search
- Provider: **local** (FlexSearch) generated at build time.
- Works offline; indexes titles, headings, and descriptions across docs.
- Search box appears in the top nav. Keyboard shortcut: `/` (press in any page).
## Content guidelines
- Every page must declare `title` and `description` frontmatter to land in the index.
- Prefer short headings (≤60 characters) for clean search snippets.
- Keep code examples deterministic: pin versions and avoid network calls.
## Upcoming
- API operation deep-links will join the index once schema viewer (DEVPORT-62-002) lands.
- Try-It console (DEVPORT-63-001) will expose a sandbox surface gated by scopes.

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

@@ -0,0 +1,30 @@
---
title: Welcome to the StellaOps DevPortal
description: Deterministic, offline-first documentation and API reference for the StellaOps platform.
---
import { Card, CardGrid } from '@astrojs/starlight/components';
The StellaOps DevPortal binds specs, runnable examples, and SDK entrypoints into a single, deterministic build. Everything here is designed to work online or fully air-gapped so auditors and engineers see the same evidence.
<CardGrid>
<Card title="Aggregate API" icon="tabler:api" href="/docs/api-reference/">
Browse the composed OpenAPI surface, schema-first paths, and auth expectations.
</Card>
<Card title="Get started" icon="tabler:flag" href="/docs/guides/getting-started/">
Install tooling, sync the aggregate spec, and render the portal locally.
</Card>
<Card title="Navigation & search" icon="tabler:search" href="/docs/guides/navigation-search/">
Learn how content is organized and how offline search works.
</Card>
</CardGrid>
## Why now
- Offline parity: the same portal ships as static HTML with bundled assets.
- Deterministic rebuilds: aggregate spec and examples are pinned in-source.
- Audit-ready: schema-first views, provenance attached to specs, and upcoming try-it sandbox.
## What lives here
- Aggregate OpenAPI (namespaced by service) with schema explorer.
- Guides for tokens, scopes, SDKs, and export bundles.
- Release notes aligned to platform drops.

View File

@@ -0,0 +1,18 @@
---
title: Release Notes
description: Drop-by-drop updates for the DevPortal surface.
---
## 2025-11 (Sprint 0206.0001.0001)
- ✅ Selected Astro + Starlight as the static site generator for deterministic offline builds.
- ✅ Added navigation scaffolding (Overview, Guides, API, Roadmap) with local search enabled.
- ✅ 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).
## How to contribute release entries
- Add a dated section with bullet points grouped by task ID when features land.
- Keep entries aligned to sprint IDs and include any risks or follow-ups.

View File

@@ -0,0 +1,61 @@
---
title: Try-It Console
description: Run authenticated requests against the sandbox API with scoped tokens and offline-ready tooling.
---
> Use this console to exercise the sandbox API. It runs fully client-side with no external assets. Supply a short-lived token with the scopes shown below. Nothing is sent to third-party services.
## Token onboarding
- Obtain a sandbox token from the Platform sandbox issuer (`/auth/oidc/token`) using the `client_credentials` flow.
- Required scopes (minimum): `stellaops.read`, `stellaops.write:sandbox`.
- Tokens should be short-lived (&lt;15 minutes); refresh before each session.
- Paste only sandbox tokens here—**never** production credentials.
<div class="token-panel">
<label for="token-input">Bearer token</label>
<input id="token-input" type="password" autocomplete="off" placeholder="Paste sandbox token" />
<div class="token-actions">
<button id="token-apply">Apply to console</button>
<button id="token-clear" class="secondary">Clear</button>
</div>
<p class="hint">Token is stored in-memory only for this tab. Reload to remove.</p>
</div>
## Sandbox server
- Base URL: `https://sandbox.api.stellaops.local`
- Operations remain namespaced by service (e.g., `/authority/health`, `/orchestrator/jobs`).
<rapi-doc
id="sandbox-rapidoc"
spec-url="/api/stella.yaml"
render-style="focused"
theme="dark"
bg-color="#0b1220"
text-color="#e5e7eb"
primary-color="#0ea5e9"
nav-bg-color="#0f172a"
nav-text-color="#cbd5e1"
show-header="false"
allow-try="true"
allow-server-selection="true"
allow-spec-url-load="false"
allow-spec-file-load="false"
api-key-name="Authorization"
api-key-location="header"
regular-font="Space Grotesk"
mono-font="JetBrains Mono"
schema-style="tree"
default-schema-tab="schema"
sort-tags="true"
sort-endpoints-by="path"
hide-schema-titles="false"
layout="column"
style="height: 78vh; border: 1px solid #1f2937; border-radius: 12px;"
></rapi-doc>
## Tips
- Set the server dropdown to `https://sandbox.api.stellaops.local` before sending requests.
- Use small payloads; responses are truncated by RapiDoc if excessively large.
- Keep retries low to preserve determinism (default is none).
<script src="/js/rapidoc-loader.js"></script>
<script src="/js/try-it-console.js"></script>

View File

@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" role="img" aria-labelledby="title desc">
<title id="title">StellaOps DevPortal</title>
<desc id="desc">Stylised starburst mark for the StellaOps developer portal.</desc>
<defs>
<linearGradient id="g" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#0ea5e9" />
<stop offset="100%" stop-color="#22d3ee" />
</linearGradient>
</defs>
<rect width="200" height="200" rx="28" fill="#0b1220" />
<path fill="url(#g)" d="M100 22l16 46h48l-39 28 15 46-40-27-40 27 15-46-39-28h48z"/>
<circle cx="100" cy="100" r="16" fill="#0b1220" stroke="#22d3ee" stroke-width="6" />
</svg>

After

Width:  |  Height:  |  Size: 679 B

View File

@@ -0,0 +1,160 @@
:root {
--sl-font-sans: "Space Grotesk", "Segoe UI", "Inter", system-ui, -apple-system, sans-serif;
--sl-font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace;
--sl-color-accent: #0ea5e9;
--sl-color-text: #e5e7eb;
--sl-color-text-accent: #a5f3fc;
--sl-color-text-muted: #cbd5e1;
--sl-color-bg: #0b1220;
--sl-color-bg-soft: #0f172a;
--sl-color-hairline: #1f2937;
--sl-heading-font-weight: 700;
--sl-body-font-weight: 400;
}
body {
background: radial-gradient(circle at 20% 20%, rgba(14, 165, 233, 0.12), transparent 25%),
radial-gradient(circle at 80% 10%, rgba(99, 102, 241, 0.14), transparent 25%),
linear-gradient(180deg, #0b1220 0%, #0f172a 60%, #0b1220 100%);
color: var(--sl-color-text);
}
.sl-link-card {
border: 1px solid var(--sl-color-hairline);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
}
:where(.sl-markdown) h2 {
letter-spacing: -0.02em;
}
:where(.sl-markdown) code {
background: rgba(15, 23, 42, 0.7);
border: 1px solid var(--sl-color-hairline);
}
nav.sl-topnav {
border-bottom: 1px solid var(--sl-color-hairline);
backdrop-filter: blur(10px);
}
.sl-search-box input {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--sl-color-hairline);
}
.version-select {
display: inline-flex;
align-items: center;
gap: 0.5rem;
margin: 1rem 0;
padding: 0.75rem 1rem;
border: 1px solid var(--sl-color-hairline);
border-radius: 12px;
background: rgba(15, 23, 42, 0.6);
}
.version-select select {
background: #0f172a;
color: var(--sl-color-text);
border: 1px solid var(--sl-color-hairline);
padding: 0.4rem 0.6rem;
border-radius: 8px;
}
.copy-snippets {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin: 1rem 0 2rem 0;
}
.copy-snippets .snippet {
border: 1px solid var(--sl-color-hairline);
border-radius: 12px;
padding: 0.75rem;
background: rgba(15, 23, 42, 0.7);
}
.copy-snippets header {
font-weight: 600;
margin-bottom: 0.5rem;
}
.copy-snippets pre {
background: rgba(0, 0, 0, 0.35);
border-radius: 8px;
padding: 0.75rem;
overflow-x: auto;
border: 1px solid var(--sl-color-hairline);
}
.copy-snippets button {
margin-top: 0.6rem;
background: var(--sl-color-accent);
color: #0b1220;
border: none;
padding: 0.4rem 0.75rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
.copy-snippets button:hover {
filter: brightness(1.05);
}
.token-panel {
border: 1px solid var(--sl-color-hairline);
border-radius: 12px;
padding: 1rem;
background: rgba(15, 23, 42, 0.7);
margin: 1rem 0;
}
.token-panel label {
font-weight: 600;
display: block;
margin-bottom: 0.35rem;
}
.token-panel input {
width: 100%;
background: #0f172a;
color: var(--sl-color-text);
border: 1px solid var(--sl-color-hairline);
border-radius: 8px;
padding: 0.5rem 0.65rem;
}
.token-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.75rem;
}
.token-actions button {
background: var(--sl-color-accent);
color: #0b1220;
border: none;
padding: 0.45rem 0.9rem;
border-radius: 8px;
font-weight: 700;
cursor: pointer;
}
.token-actions button.secondary {
background: transparent;
color: var(--sl-color-text);
border: 1px solid var(--sl-color-hairline);
}
.token-actions button:hover {
filter: brightness(1.05);
}
.hint {
margin-top: 0.4rem;
color: var(--sl-color-text-muted);
}

View File

@@ -0,0 +1,7 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"types": ["astro/client"],
"baseUrl": "."
}
}