mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
docs(api): OpenAPI spec, Redoc at /docs, CI spec-lint
- docs/openapi.yaml: the public, token-reachable surface only, with the auth model (Bearer st_) and a per-operation x-required-scope (read<write<full). JWT-only routers are excluded by design. - Serve /openapi.yaml + /docs (Redoc via a vendored standalone bundle, no CDN so it works air-gapped; /docs is CSP-exempt). docs/ is bundled into the release tarball. - CI: redocly lint + a public-only guard that fails loudly if a JWT-only path ever leaks into the spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dce0d22763
commit
c1b9c27f3a
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
|
|
@ -33,6 +33,27 @@ jobs:
|
|||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
openapi:
|
||||
name: OpenAPI spec lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Lint the public API spec
|
||||
run: npx --yes @redocly/cli@latest lint docs/openapi.yaml
|
||||
# Contract integrity: the spec documents ONLY the token-reachable public surface.
|
||||
# A JWT-only router (admin/auth/provision/...) appearing here is a security flag,
|
||||
# not a convenience - fail loudly. (The runtime partition test is a separate suite
|
||||
# that will cross-check the spec against the live mount list.)
|
||||
- name: Assert spec is public-only
|
||||
run: |
|
||||
BAD=$(grep -oE '^ /(admin|auth|workspaces|ai|provision|white-label|status|subscription|stripe|teams|player-debug|contact|tokens)\b' docs/openapi.yaml || true)
|
||||
if [ -n "$BAD" ]; then echo "::error::JWT-only path(s) leaked into the public spec:"; echo "$BAD"; exit 1; fi
|
||||
if grep -qE 'unassigned|/prune' docs/openapi.yaml; then echo "::error::token-denied endpoint present in public spec"; exit 1; fi
|
||||
echo "OK: spec is public-only"
|
||||
|
||||
android-test:
|
||||
name: Android unit tests (Kotlin schedule evaluator vectors)
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -87,7 +87,7 @@ jobs:
|
|||
--exclude='*.db' --exclude='*.db-wal' --exclude='*.db-shm' --exclude='*.db.*' \
|
||||
--exclude='server/uploads' --exclude='server/certs' --exclude='server/test' \
|
||||
--exclude='*.apk' \
|
||||
server frontend scripts VERSION README.md LICENSE .env.example ScreenTinker.wgt
|
||||
server frontend scripts docs VERSION README.md LICENSE .env.example ScreenTinker.wgt
|
||||
echo "TARBALL=$OUT" >> "$GITHUB_ENV"
|
||||
ls -la "$OUT"
|
||||
|
||||
|
|
|
|||
1728
docs/openapi.yaml
Normal file
1728
docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load diff
17
frontend/api-docs.html
Normal file
17
frontend/api-docs.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>ScreenTinker API Reference</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="description" content="ScreenTinker public API reference"/>
|
||||
<style>body { margin: 0; padding: 0; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Self-hosted Redoc: the spec is served at /openapi.yaml and the Redoc bundle is
|
||||
vendored locally (no CDN) so the docs work on an offline/air-gapped instance.
|
||||
The <redoc> element auto-initialises from the standalone bundle. -->
|
||||
<redoc spec-url="/openapi.yaml"></redoc>
|
||||
<script src="/vendor/redoc.standalone.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1838
frontend/vendor/redoc.standalone.js
vendored
Normal file
1838
frontend/vendor/redoc.standalone.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -99,6 +99,7 @@ app.use(helmet({
|
|||
app.use((req, res, next) => {
|
||||
if (req.path === '/' || req.path === '/landing.html') return next();
|
||||
if (req.path.startsWith('/player')) return next();
|
||||
if (req.path === '/docs') return next(); // Redoc API reference needs a relaxed CSP
|
||||
if (req.path.startsWith('/api/widgets/') && req.path.endsWith('/render')) return next();
|
||||
if (req.path.startsWith('/api/kiosk/') && req.path.endsWith('/render')) return next();
|
||||
return dashboardCsp(req, res, next);
|
||||
|
|
@ -188,6 +189,19 @@ app.get('/robots.txt', (req, res) => {
|
|||
res.sendFile(path.join(config.frontendDir, 'robots.txt'));
|
||||
});
|
||||
|
||||
// Public API reference. /openapi.yaml is the machine-readable contract (served from
|
||||
// docs/); /docs is the Redoc viewer (frontend/api-docs.html + the vendored standalone
|
||||
// bundle under /vendor, no CDN so it works air-gapped). /docs is CSP-exempt above
|
||||
// because Redoc needs a relaxed policy.
|
||||
app.get('/openapi.yaml', (req, res) => {
|
||||
res.type('text/yaml');
|
||||
res.setHeader('Cache-Control', 'public, max-age=300');
|
||||
res.sendFile(path.join(__dirname, '..', 'docs', 'openapi.yaml'));
|
||||
});
|
||||
app.get('/docs', (req, res) => {
|
||||
res.sendFile(path.join(config.frontendDir, 'api-docs.html'));
|
||||
});
|
||||
|
||||
// Serve frontend static files
|
||||
// JS/CSS/HTML: no-cache (always revalidate, uses ETag/304)
|
||||
// Images/fonts/icons: long cache for Cloudflare + browser
|
||||
|
|
|
|||
Loading…
Reference in a new issue