openvibers/openvibe.codes Commits
API explorer (WS-C task 6): /docs/api lists every service's OpenAPI 3.1 document from openvibe-contracts 0.51.0; /docs/api/:service shows each route with its method, capabilities, visibility, parameters, request and response schemas (linked to their contract pages) and a public-only filter; /docs/api/:service.json serves the document with CORS *; capability pages link their routes; in the sitemap
Changes
@@@ -16,7 +16,7 @@"express": "^4.21.2","express-rate-limit": "^7.5.0","helmet": "^8.1.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.51.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0"},@@@ -1023,9 +1023,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.49.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",-- "integrity": "sha512-lapsLoBm3ZpndgiOMELdCBh2PJtuuyqj6SUhQsfwJkxnLdQfyhXfRFgiuLh+iQrg4kqtG9611191Mp8zAfk7fQ==",++ "version": "0.51.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.51.0",++ "integrity": "sha512-XH+WF0iRJcThoNYE3Ys4oL52BzFwnyPX4Bzsq3MqJvDqdMWTQydvOTTHxdSbbtsTo7CIOZk3QNVy1HovyTId0A==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -27,7 +27,7 @@"express": "^4.21.2","express-rate-limit": "^7.5.0","helmet": "^8.1.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.51.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0"},@@@ -39,6 +39,13 @@ ol.steps li { margin: .4rem 0; }.badge.ok { border-color: var(--success, #22c55e); color: var(--success, #22c55e); }.badge.warn { border-color: var(--warning, #f59e0b); color: var(--warning, #f59e0b); }.badge.bad { border-color: var(--error, #ef4444); color: var(--error, #ef4444); }++/* API explorer: one block per route, the method as a badge */++.api-op { border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 10px 14px; margin: 12px 0; background: var(--bg-card, #131c2e); scroll-margin-top: 72px; }++.api-op h3 { margin: 0 0 4px; font-size: 1rem; overflow-wrap: anywhere; }++.api-op .facts { margin: 6px 0; }++.badge.method-get { border-color: var(--success, #22c55e); color: var(--success, #22c55e); }++.badge.method-post, .badge.method-put, .badge.method-patch { border-color: var(--accent-light, #60a5fa); color: var(--accent-light, #60a5fa); }++.badge.method-delete { border-color: var(--error, #ef4444); color: var(--error, #ef4444); }.notice, .problem, .result { border-left: 3px solid var(--info, #38bdf8); padding: 8px 12px; margin: 12px 0; background: var(--bg-card, #131c2e); border-radius: 0 8px 8px 0; }.notice.ok, .result.ok { border-left-color: var(--success, #22c55e); }.notice.warn { border-left-color: var(--warning, #f59e0b); }@@@ -8,6 +8,8 @@* /docs/contracts[/:id] catalog; one page per contract: field table, fixtures, raw schema* /docs/contracts/:id.json the schema itself* /docs/capabilities[/:id] capability catalog; grantable (public/partner, active) highlighted++ * /docs/api[/:service] API explorer: every service's routes from openvibe-contracts' OpenAPI++ * /docs/api/:service.json that OpenAPI 3.1 document (CORS *, for Swagger-style tools)* /docs/events event types from the service manifests* /docs/services the Network's registry with health as reported (never invented)* /docs/sdk[/:module] SDK reference from its .d.ts files@@@ -21,6 +23,9 @@ const { markdown } = require('../render/markdown');function createDocsRoutes(ctx) {const { docs, config, network } = ctx;const r = asyncRouter();++ const openapi = require('openvibe-contracts').openapi;++ const apiIndex = openapi.index();++ const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));const PUBLIC_CACHE = 'public, max-age=300';const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');@@@ -39,6 +44,7 @@ function createDocsRoutes(ctx) {<p>Nothing on these pages is written by hand: each one is rendered from the published packages above when Codes starts, so it always matches what the platform runs against. If something is missing here, it is not part of the public surface yet (<a href="/policy/compatibility">why</a>).</p><ul class="cards"><li><a href="/docs/contracts"><strong>Contracts</strong></a><span>${docs.contracts.length} JSON Schemas with fields, examples and versions</span></li>++<li><a href="/docs/api"><strong>API explorer</strong></a><span>${apiIndex.reduce((n, s) => n + s.operations, 0)} routes across ${apiIndex.length} services, with their capabilities and schemas (OpenAPI 3.1)</span></li><li><a href="/docs/capabilities"><strong>Capabilities</strong></a><span>${docs.capabilities.length} capabilities; ${grantable} can be granted to apps</span></li><li><a href="/docs/events"><strong>Events</strong></a><span>${docs.events.length} event types services declare they produce</span></li><li><a href="/docs/services"><strong>Services</strong></a><span>the registry as OpenVibe.Network reports it, with health</span></li>@@@ -144,7 +150,86 @@ ${c.grantable ? html`<div class="notice ok">Grantable to apps: request it for an${c.inputSchema ? html`<dt>Input</dt><dd>${docs.contract(c.inputSchema.split('@')[0]) ? html`<a href="/docs/contracts/${c.inputSchema.split('@')[0]}">${c.inputSchema}</a>` : c.inputSchema}</dd>` : ''}${c.outputSchema ? html`<dt>Output</dt><dd>${c.outputSchema}</dd>` : ''}<dt>Events</dt><dd>${c.events.length ? c.events.join(', ') : '—'}</dd>--<dt>Implemented by</dt><dd>${(c.implementedBy || []).length ? html`<ul class="plain">${c.implementedBy.map((x) => html`<li><code>${x}</code></li>`)}</ul>` : '—'}</dd></dl>`,++<dt>Implemented by</dt><dd>${(c.implementedBy || []).length ? html`<ul class="plain">${c.implementedBy.map((x) => html`<li><code>${x}</code></li>`)}</ul>` : '—'}</dd></dl>++${apiDocs.has(c.owner) ? html`<p><a href="/docs/api/${c.owner}#cap-${c.id}">These routes in the API explorer</a></p>` : ''}`,++ });++ });++++ // ── API explorer (WS-C task 6): openvibe-contracts' OpenAPI 3.1 per service ──++ const METHOD_ORDER = ['get', 'head', 'post', 'put', 'patch', 'delete'];++ const schemaLink = (s) => {++ if (!s || typeof s !== 'object') return '—';++ if (s.$ref) {++ const key = String(s.$ref).replace('#/components/schemas/', '');++ const id = key.replace(/\.v\d+$/, '');++ return docs.contract(id) ? html`<a href="/docs/contracts/${id}">${id}@${key.split('.v').pop()}</a>` : code(key);++ }++ if (s.anyOf) return html`one of ${s.anyOf.map((x, i) => html`${i ? ', ' : ''}${schemaLink(x)}`)}`;++ if (s.contentEncoding === 'binary') return 'bytes';++ return code(s.type || 'schema');++ };++ const bodyOf = (content) => (content ? Object.entries(content).map(([type, m]) => html`<div><code>${type}</code> ${schemaLink(m.schema)}</div>`) : '—');++++ r.get('/api', (req, res) => {++ page(req, res, {++ title: 'API explorer',++ description: `Every OpenVibe service route the contracts describe, with its capabilities and schemas: ${apiIndex.length} OpenAPI 3.1 documents.`,++ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer' }],++ body: html`<h1>API explorer</h1>${versions()}++<p>One OpenAPI 3.1 document per service, generated from the capabilities in openvibe-contracts: every route a capability names, the capabilities it performs (a token for the service must carry one), its request and response schemas and its problem+json errors. A service may answer more routes than these; what is here is the contract.</p>++${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map((s) => [++ html`<a href="/docs/api/${s.service}">${s.name}</a>`, String(s.operations), String(s.capabilities),++ s.origin ? code(s.origin) : '—', html`<a href="/docs/api/${s.service}.json">${s.service}.json</a>`,++ ]))}++<p class="muted">Load a document into any OpenAPI tool from <code>https://openvibe.codes/docs/api/<service>.json</code>, or read it from the package: <code>require('openvibe-contracts').openapi.document('tools')</code>.</p>`,++ });++ });++++ r.get('/api/:service.json', (req, res, next) => {++ const doc = apiDocs.get(req.params.service);++ if (!doc) return next();++ res.set({ 'Cache-Control': PUBLIC_CACHE, 'Access-Control-Allow-Origin': '*' }).type('application/vnd.oai.openapi+json;version=3.1').send(JSON.stringify(doc, null, 2));++ });++++ r.get('/api/:service', (req, res, next) => {++ const doc = apiDocs.get(req.params.service);++ if (!doc) return next();++ const s = apiIndex.find((x) => x.service === req.params.service);++ const onlyPublic = req.query.public === '1';++ const ops = [];++ for (const [p, methods] of Object.entries(doc.paths)) {++ for (const m of Object.keys(methods).sort((a, b) => METHOD_ORDER.indexOf(a) - METHOD_ORDER.indexOf(b))) {++ const op = methods[m];++ if (onlyPublic && !op['x-openvibe-visibility'].some((v) => v === 'public' || v === 'partner')) continue;++ ops.push({ path: p, method: m, op });++ }++ }++ const firstCap = new Set();++ const opBlock = ({ path: p, method, op }) => {++ const anchors = op['x-openvibe-capabilities'].filter((id) => !firstCap.has(id));++ anchors.forEach((id) => firstCap.add(id));++ const params = op.parameters || [];++ return html`<section class="api-op" id="${op.operationId}">${anchors.map((id) => html`<span id="cap-${id}"></span>`)}++<h3><span class="badge method-${method}">${method.toUpperCase()}</span> <code>${p}</code></h3>++<p>${op.summary}</p>++<dl class="facts"><dt>Capabilities</dt><dd>${op['x-openvibe-capabilities'].map((id, i) => html`${i ? ', ' : ''}<a href="/docs/capabilities/${id}">${id}</a>`)}${op.security.some((x) => !Object.keys(x).length) ? html` ${badge('open', 'ok')}` : ''}</dd>++<dt>Visibility</dt><dd>${op['x-openvibe-visibility'].join(', ')}</dd>++${params.length ? html`<dt>Parameters</dt><dd><ul class="plain">${params.map((x) => html`<li><code>${x.name}</code> (${x.in}${x.required ? ', required' : ''})${x.description ? html` — ${x.description}` : ''}</li>`)}</ul></dd>` : ''}++${op.requestBody ? html`<dt>Request body</dt><dd>${bodyOf(op.requestBody.content)}</dd>` : ''}++${op['x-openvibe-input'] && !op.requestBody ? html`<dt>Input</dt><dd>${schemaLink(op['x-openvibe-input'])}</dd>` : ''}++<dt>Response</dt><dd>${bodyOf(op.responses['2XX'].content)}</dd>++<dt>Errors</dt><dd><a href="/docs/contracts/errors.problem">errors.problem@1</a> (application/problem+json)</dd></dl>++<details><summary>Description</summary>${markdown(op.description)}</details></section>`;++ };++ page(req, res, {++ title: `${s.name} API`,++ description: `${s.operations} routes of ${s.name} with their capabilities and schemas (OpenAPI 3.1 from openvibe-contracts v${docs.contractsVersion}).`,++ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer', href: '/docs/api' }, { label: s.name }],++ body: html`<h1>${s.name} API</h1>${versions()}++<p>${doc.servers ? html`Server <code>${doc.servers[0].url}</code>. ` : ''}${s.operations} routes performing ${s.capabilities} capabilities. <a href="/docs/api/${s.service}.json">OpenAPI 3.1 document</a>.</p>++<form method="get" action="/docs/api/${s.service}" class="inline-form"><label><input type="checkbox" name="public" value="1"${onlyPublic ? raw(' checked') : ''}> only routes apps can be granted (public, partner)</label> <button type="submit">Filter</button></form>++${ops.length ? ops.map(opBlock) : html`<p class="muted">No route here matches.</p>`}++${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><ul class="plain">${doc['x-openvibe-other-bindings'].map((b) => html`<li><a href="/docs/capabilities/${b.capability}">${b.capability}</a>: <code>${b.binding}</code></li>`)}</ul>` : ''}`,});});@@@ -74,7 +74,7 @@ function createPageRoutes(ctx) {<li>Publish release metadata with a <a href="/manifests/validate">validated manifest</a>; rotate or revoke credentials any time.</li></ol><h2>Reference</h2>--<p>Generated from <code>openvibe-contracts v${docs.contractsVersion}</code> and <code>openvibe-sdk v${docs.sdkVersion}</code>: <a href="/docs/contracts">contracts</a> · <a href="/docs/capabilities">capabilities</a> · <a href="/docs/events">events</a> · <a href="/docs/services">services</a> · <a href="/docs/tools">tools</a> · <a href="/docs/sdk">SDK</a>.</p>++<p>Generated from <code>openvibe-contracts v${docs.contractsVersion}</code> and <code>openvibe-sdk v${docs.sdkVersion}</code>: <a href="/docs/api">API explorer</a> · <a href="/docs/contracts">contracts</a> · <a href="/docs/capabilities">capabilities</a> · <a href="/docs/events">events</a> · <a href="/docs/services">services</a> · <a href="/docs/tools">tools</a> · <a href="/docs/sdk">SDK</a>.</p><h2>Recent releases</h2>${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((x) => [html`<a href="/apps/${x.app_id}">${x.name}</a>`, x.kind, html`<a href="/releases/${x.id}">${x.version}</a>`, x.status, x.trust.tier, time(x.published_at),@@@ -271,10 +271,11 @@ ${signedIn ? releaseActions(req, rel) : ''}].join('\n'));});r.get('/sitemap.xml', (req, res) => {-- const urls = ['/', '/docs', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate',++ const urls = ['/', '/docs', '/docs/api', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate','/policy', '/policy/rfc', '/policy/compatibility', '/policy/licensing', '/policy/transparency',...governance.filter((g) => !g.draft).map((g) => `/policy/${g.slug}`),...docs.contracts.map((c) => `/docs/contracts/${c.id}`), ...docs.capabilities.map((c) => `/docs/capabilities/${c.id}`),++ ...require('openvibe-contracts').openapi.index().map((x) => `/docs/api/${x.service}`),...docs.sdk.map((m) => `/docs/sdk/${m.slug}`), ...docs.adrs.map((a) => `/docs/adr/${a.id}`)];const x = (s) => s.replace(/&/g, '&').replace(/</g, '<');res.set('Cache-Control', 'public, max-age=3600').type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => `<url><loc>${x(config.baseUrl + u)}</loc></url>`).join('\n')}\n</urlset>\n`);@@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];-- assert.strictEqual(tag('openvibe-contracts'), 'v0.49.0');++ assert.strictEqual(tag('openvibe-contracts'), 'v0.51.0');assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);assert.strictEqual(sdkPkg.version, '0.11.0');-- // The contracts tag v0.49.0 carries the released codes manifest and codes.app-manifest@1.++ // The contracts tag v0.51.0 carries the released codes manifest and codes.app-manifest@1.assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');assert.ok(contracts.resolve('codes.app-manifest@1'));});@@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');const r = await t.get(p);assert.strictEqual(r.status, 200, p);assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);-- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.49.0'), `${p} links the pinned tag`);-- if (contractsVersion !== '0.49.0') assert.ok(r.text.includes('(tag v0.49.0)'), `${p} says the tag differs from the package version`);++ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.51.0'), `${p} links the pinned tag`);++ if (contractsVersion !== '0.51.0') assert.ok(r.text.includes('(tag v0.51.0)'), `${p} says the tag differs from the package version`);assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);}const v = (await t.get('/api/v1/docs/versions')).json();assert.strictEqual(v.contracts, contractsVersion);-- assert.strictEqual(v.contracts_tag, 'v0.49.0');++ assert.strictEqual(v.contracts_tag, 'v0.51.0');assert.strictEqual(v.sdk, sdkPkg.version);});@@@ -105,6 +105,35 @@ const sdkPkg = require('openvibe-sdk/package.json');assert.match(r.headers.get('cache-control'), /public/);const sm = await t.get('/sitemap.xml');assert.ok(sm.text.includes('/docs/contracts/mods.mod-manifest</loc>'));++ assert.ok(sm.text.includes('/docs/api</loc>') && sm.text.includes('/docs/api/tools</loc>'), 'the API explorer is in the sitemap');++ });++++ await check('the API explorer lists every service document from the pinned contracts (WS-C task 6)', async () => {++ const index = contracts.openapi.index();++ assert.ok(index.length >= 20);++ const list = await t.get('/docs/api');++ assert.strictEqual(list.status, 200);++ for (const s of index) assert.ok(list.text.includes(`href="/docs/api/${s.service}"`), `${s.service} is listed`);++ assert.ok((await t.get('/docs')).text.includes('href="/docs/api"'), 'the docs index links it');++ const raw = await t.get('/docs/api/tools.json');++ assert.strictEqual(raw.status, 200);++ assert.match(raw.headers.get('content-type'), /application\/vnd\.oai\.openapi\+json/);++ assert.strictEqual(raw.headers.get('access-control-allow-origin'), '*');++ assert.deepStrictEqual(raw.json(), contracts.openapi.document('tools'), 'the document as the package ships it');++ const page = await t.get('/docs/api/tools');++ assert.strictEqual(page.status, 200);++ const doc = contracts.openapi.document('tools');++ const ops = Object.values(doc.paths).reduce((n, m) => n + Object.keys(m).length, 0);++ assert.strictEqual((page.text.match(/class="api-op"/g) || []).length, ops, 'one block per route');++ assert.ok(page.text.includes('id="cap-tools.job.create"') && page.text.includes('href="/docs/capabilities/tools.job.create"'), 'capabilities anchored and linked');++ assert.ok(page.text.includes('href="/docs/contracts/tools.job"'), 'schemas link to their contract pages');++ assert.ok(page.text.includes('<code>/api/v1/jobs/{id}</code>'));++ const pub = await t.get('/docs/api/tools?public=1');++ assert.ok((pub.text.match(/class="api-op"/g) || []).length <= ops);++ assert.strictEqual((await t.get('/docs/api/nope')).status, 404);++ assert.strictEqual((await t.get('/docs/api/nope.json')).status, 404);++ const cap = await t.get('/docs/capabilities/tools.job.create');++ assert.ok(cap.text.includes('href="/docs/api/tools#cap-tools.job.create"'), 'a capability links its routes');});await check('pages rendered for a signed-in person are never publicly cacheable', async () => {
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.51.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0"
},
},
@@ -1023,9 +1023,9 @@
@@ -1023,9 +1023,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.49.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
- "integrity": "sha512-lapsLoBm3ZpndgiOMELdCBh2PJtuuyqj6SUhQsfwJkxnLdQfyhXfRFgiuLh+iQrg4kqtG9611191Mp8zAfk7fQ==",
+ "version": "0.51.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.51.0",
+ "integrity": "sha512-XH+WF0iRJcThoNYE3Ys4oL52BzFwnyPX4Bzsq3MqJvDqdMWTQydvOTTHxdSbbtsTo7CIOZk3QNVy1HovyTId0A==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -27,7 +27,7 @@
@@ -27,7 +27,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.49.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.51.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0"
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.13.0"
},
},
@@ -39,6 +39,13 @@ ol.steps li { margin: .4rem 0; }
@@ -39,6 +39,13 @@ ol.steps li { margin: .4rem 0; }
.badge.ok { border-color: var(--success, #22c55e); color: var(--success, #22c55e); }
.badge.ok { border-color: var(--success, #22c55e); color: var(--success, #22c55e); }
.badge.warn { border-color: var(--warning, #f59e0b); color: var(--warning, #f59e0b); }
.badge.warn { border-color: var(--warning, #f59e0b); color: var(--warning, #f59e0b); }
.badge.bad { border-color: var(--error, #ef4444); color: var(--error, #ef4444); }
.badge.bad { border-color: var(--error, #ef4444); color: var(--error, #ef4444); }
+/* API explorer: one block per route, the method as a badge */
+.api-op { border: 1px solid var(--border, #1f2d47); border-radius: 10px; padding: 10px 14px; margin: 12px 0; background: var(--bg-card, #131c2e); scroll-margin-top: 72px; }
+.api-op h3 { margin: 0 0 4px; font-size: 1rem; overflow-wrap: anywhere; }
+.api-op .facts { margin: 6px 0; }
+.badge.method-get { border-color: var(--success, #22c55e); color: var(--success, #22c55e); }
+.badge.method-post, .badge.method-put, .badge.method-patch { border-color: var(--accent-light, #60a5fa); color: var(--accent-light, #60a5fa); }
+.badge.method-delete { border-color: var(--error, #ef4444); color: var(--error, #ef4444); }
.notice, .problem, .result { border-left: 3px solid var(--info, #38bdf8); padding: 8px 12px; margin: 12px 0; background: var(--bg-card, #131c2e); border-radius: 0 8px 8px 0; }
.notice, .problem, .result { border-left: 3px solid var(--info, #38bdf8); padding: 8px 12px; margin: 12px 0; background: var(--bg-card, #131c2e); border-radius: 0 8px 8px 0; }
.notice.ok, .result.ok { border-left-color: var(--success, #22c55e); }
.notice.ok, .result.ok { border-left-color: var(--success, #22c55e); }
.notice.warn { border-left-color: var(--warning, #f59e0b); }
.notice.warn { border-left-color: var(--warning, #f59e0b); }
@@ -8,6 +8,8 @@
@@ -8,6 +8,8 @@
* /docs/contracts[/:id] catalog; one page per contract: field table, fixtures, raw schema
* /docs/contracts[/:id] catalog; one page per contract: field table, fixtures, raw schema
* /docs/contracts/:id.json the schema itself
* /docs/contracts/:id.json the schema itself
* /docs/capabilities[/:id] capability catalog; grantable (public/partner, active) highlighted
* /docs/capabilities[/:id] capability catalog; grantable (public/partner, active) highlighted
+ * /docs/api[/:service] API explorer: every service's routes from openvibe-contracts' OpenAPI
+ * /docs/api/:service.json that OpenAPI 3.1 document (CORS *, for Swagger-style tools)
* /docs/events event types from the service manifests
* /docs/events event types from the service manifests
* /docs/services the Network's registry with health as reported (never invented)
* /docs/services the Network's registry with health as reported (never invented)
* /docs/sdk[/:module] SDK reference from its .d.ts files
* /docs/sdk[/:module] SDK reference from its .d.ts files
@@ -21,6 +23,9 @@ const { markdown } = require('../render/markdown');
@@ -21,6 +23,9 @@ const { markdown } = require('../render/markdown');
function createDocsRoutes(ctx) {
function createDocsRoutes(ctx) {
const { docs, config, network } = ctx;
const { docs, config, network } = ctx;
const r = asyncRouter();
const r = asyncRouter();
+ const openapi = require('openvibe-contracts').openapi;
+ const apiIndex = openapi.index();
+ const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));
const PUBLIC_CACHE = 'public, max-age=300';
const PUBLIC_CACHE = 'public, max-age=300';
const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');
const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');
@@ -39,6 +44,7 @@ function createDocsRoutes(ctx) {
@@ -39,6 +44,7 @@ function createDocsRoutes(ctx) {
<p>Nothing on these pages is written by hand: each one is rendered from the published packages above when Codes starts, so it always matches what the platform runs against. If something is missing here, it is not part of the public surface yet (<a href="/policy/compatibility">why</a>).</p>
<p>Nothing on these pages is written by hand: each one is rendered from the published packages above when Codes starts, so it always matches what the platform runs against. If something is missing here, it is not part of the public surface yet (<a href="/policy/compatibility">why</a>).</p>
<ul class="cards">
<ul class="cards">
<li><a href="/docs/contracts"><strong>Contracts</strong></a><span>${docs.contracts.length} JSON Schemas with fields, examples and versions</span></li>
<li><a href="/docs/contracts"><strong>Contracts</strong></a><span>${docs.contracts.length} JSON Schemas with fields, examples and versions</span></li>
+<li><a href="/docs/api"><strong>API explorer</strong></a><span>${apiIndex.reduce((n, s) => n + s.operations, 0)} routes across ${apiIndex.length} services, with their capabilities and schemas (OpenAPI 3.1)</span></li>
<li><a href="/docs/capabilities"><strong>Capabilities</strong></a><span>${docs.capabilities.length} capabilities; ${grantable} can be granted to apps</span></li>
<li><a href="/docs/capabilities"><strong>Capabilities</strong></a><span>${docs.capabilities.length} capabilities; ${grantable} can be granted to apps</span></li>
<li><a href="/docs/events"><strong>Events</strong></a><span>${docs.events.length} event types services declare they produce</span></li>
<li><a href="/docs/events"><strong>Events</strong></a><span>${docs.events.length} event types services declare they produce</span></li>
<li><a href="/docs/services"><strong>Services</strong></a><span>the registry as OpenVibe.Network reports it, with health</span></li>
<li><a href="/docs/services"><strong>Services</strong></a><span>the registry as OpenVibe.Network reports it, with health</span></li>
@@ -144,7 +150,86 @@ ${c.grantable ? html`<div class="notice ok">Grantable to apps: request it for an
@@ -144,7 +150,86 @@ ${c.grantable ? html`<div class="notice ok">Grantable to apps: request it for an
${c.inputSchema ? html`<dt>Input</dt><dd>${docs.contract(c.inputSchema.split('@')[0]) ? html`<a href="/docs/contracts/${c.inputSchema.split('@')[0]}">${c.inputSchema}</a>` : c.inputSchema}</dd>` : ''}
${c.inputSchema ? html`<dt>Input</dt><dd>${docs.contract(c.inputSchema.split('@')[0]) ? html`<a href="/docs/contracts/${c.inputSchema.split('@')[0]}">${c.inputSchema}</a>` : c.inputSchema}</dd>` : ''}
${c.outputSchema ? html`<dt>Output</dt><dd>${c.outputSchema}</dd>` : ''}
${c.outputSchema ? html`<dt>Output</dt><dd>${c.outputSchema}</dd>` : ''}
<dt>Events</dt><dd>${c.events.length ? c.events.join(', ') : '—'}</dd>
<dt>Events</dt><dd>${c.events.length ? c.events.join(', ') : '—'}</dd>
-<dt>Implemented by</dt><dd>${(c.implementedBy || []).length ? html`<ul class="plain">${c.implementedBy.map((x) => html`<li><code>${x}</code></li>`)}</ul>` : '—'}</dd></dl>`,
+<dt>Implemented by</dt><dd>${(c.implementedBy || []).length ? html`<ul class="plain">${c.implementedBy.map((x) => html`<li><code>${x}</code></li>`)}</ul>` : '—'}</dd></dl>
+${apiDocs.has(c.owner) ? html`<p><a href="/docs/api/${c.owner}#cap-${c.id}">These routes in the API explorer</a></p>` : ''}`,
+ });
+ });
+
+ // ── API explorer (WS-C task 6): openvibe-contracts' OpenAPI 3.1 per service ──
+ const METHOD_ORDER = ['get', 'head', 'post', 'put', 'patch', 'delete'];
+ const schemaLink = (s) => {
+ if (!s || typeof s !== 'object') return '—';
+ if (s.$ref) {
+ const key = String(s.$ref).replace('#/components/schemas/', '');
+ const id = key.replace(/\.v\d+$/, '');
+ return docs.contract(id) ? html`<a href="/docs/contracts/${id}">${id}@${key.split('.v').pop()}</a>` : code(key);
+ }
+ if (s.anyOf) return html`one of ${s.anyOf.map((x, i) => html`${i ? ', ' : ''}${schemaLink(x)}`)}`;
+ if (s.contentEncoding === 'binary') return 'bytes';
+ return code(s.type || 'schema');
+ };
+ const bodyOf = (content) => (content ? Object.entries(content).map(([type, m]) => html`<div><code>${type}</code> ${schemaLink(m.schema)}</div>`) : '—');
+
+ r.get('/api', (req, res) => {
+ page(req, res, {
+ title: 'API explorer',
+ description: `Every OpenVibe service route the contracts describe, with its capabilities and schemas: ${apiIndex.length} OpenAPI 3.1 documents.`,
+ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer' }],
+ body: html`<h1>API explorer</h1>${versions()}
+<p>One OpenAPI 3.1 document per service, generated from the capabilities in openvibe-contracts: every route a capability names, the capabilities it performs (a token for the service must carry one), its request and response schemas and its problem+json errors. A service may answer more routes than these; what is here is the contract.</p>
+${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map((s) => [
+ html`<a href="/docs/api/${s.service}">${s.name}</a>`, String(s.operations), String(s.capabilities),
+ s.origin ? code(s.origin) : '—', html`<a href="/docs/api/${s.service}.json">${s.service}.json</a>`,
+ ]))}
+<p class="muted">Load a document into any OpenAPI tool from <code>https://openvibe.codes/docs/api/<service>.json</code>, or read it from the package: <code>require('openvibe-contracts').openapi.document('tools')</code>.</p>`,
+ });
+ });
+
+ r.get('/api/:service.json', (req, res, next) => {
+ const doc = apiDocs.get(req.params.service);
+ if (!doc) return next();
+ res.set({ 'Cache-Control': PUBLIC_CACHE, 'Access-Control-Allow-Origin': '*' }).type('application/vnd.oai.openapi+json;version=3.1').send(JSON.stringify(doc, null, 2));
+ });
+
+ r.get('/api/:service', (req, res, next) => {
+ const doc = apiDocs.get(req.params.service);
+ if (!doc) return next();
+ const s = apiIndex.find((x) => x.service === req.params.service);
+ const onlyPublic = req.query.public === '1';
+ const ops = [];
+ for (const [p, methods] of Object.entries(doc.paths)) {
+ for (const m of Object.keys(methods).sort((a, b) => METHOD_ORDER.indexOf(a) - METHOD_ORDER.indexOf(b))) {
+ const op = methods[m];
+ if (onlyPublic && !op['x-openvibe-visibility'].some((v) => v === 'public' || v === 'partner')) continue;
+ ops.push({ path: p, method: m, op });
+ }
+ }
+ const firstCap = new Set();
+ const opBlock = ({ path: p, method, op }) => {
+ const anchors = op['x-openvibe-capabilities'].filter((id) => !firstCap.has(id));
+ anchors.forEach((id) => firstCap.add(id));
+ const params = op.parameters || [];
+ return html`<section class="api-op" id="${op.operationId}">${anchors.map((id) => html`<span id="cap-${id}"></span>`)}
+<h3><span class="badge method-${method}">${method.toUpperCase()}</span> <code>${p}</code></h3>
+<p>${op.summary}</p>
+<dl class="facts"><dt>Capabilities</dt><dd>${op['x-openvibe-capabilities'].map((id, i) => html`${i ? ', ' : ''}<a href="/docs/capabilities/${id}">${id}</a>`)}${op.security.some((x) => !Object.keys(x).length) ? html` ${badge('open', 'ok')}` : ''}</dd>
+<dt>Visibility</dt><dd>${op['x-openvibe-visibility'].join(', ')}</dd>
+${params.length ? html`<dt>Parameters</dt><dd><ul class="plain">${params.map((x) => html`<li><code>${x.name}</code> (${x.in}${x.required ? ', required' : ''})${x.description ? html` — ${x.description}` : ''}</li>`)}</ul></dd>` : ''}
+${op.requestBody ? html`<dt>Request body</dt><dd>${bodyOf(op.requestBody.content)}</dd>` : ''}
+${op['x-openvibe-input'] && !op.requestBody ? html`<dt>Input</dt><dd>${schemaLink(op['x-openvibe-input'])}</dd>` : ''}
+<dt>Response</dt><dd>${bodyOf(op.responses['2XX'].content)}</dd>
+<dt>Errors</dt><dd><a href="/docs/contracts/errors.problem">errors.problem@1</a> (application/problem+json)</dd></dl>
+<details><summary>Description</summary>${markdown(op.description)}</details></section>`;
+ };
+ page(req, res, {
+ title: `${s.name} API`,
+ description: `${s.operations} routes of ${s.name} with their capabilities and schemas (OpenAPI 3.1 from openvibe-contracts v${docs.contractsVersion}).`,
+ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer', href: '/docs/api' }, { label: s.name }],
+ body: html`<h1>${s.name} API</h1>${versions()}
+<p>${doc.servers ? html`Server <code>${doc.servers[0].url}</code>. ` : ''}${s.operations} routes performing ${s.capabilities} capabilities. <a href="/docs/api/${s.service}.json">OpenAPI 3.1 document</a>.</p>
+<form method="get" action="/docs/api/${s.service}" class="inline-form"><label><input type="checkbox" name="public" value="1"${onlyPublic ? raw(' checked') : ''}> only routes apps can be granted (public, partner)</label> <button type="submit">Filter</button></form>
+${ops.length ? ops.map(opBlock) : html`<p class="muted">No route here matches.</p>`}
+${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><ul class="plain">${doc['x-openvibe-other-bindings'].map((b) => html`<li><a href="/docs/capabilities/${b.capability}">${b.capability}</a>: <code>${b.binding}</code></li>`)}</ul>` : ''}`,
});
});
});
});
@@ -74,7 +74,7 @@ function createPageRoutes(ctx) {
@@ -74,7 +74,7 @@ function createPageRoutes(ctx) {
<li>Publish release metadata with a <a href="/manifests/validate">validated manifest</a>; rotate or revoke credentials any time.</li>
<li>Publish release metadata with a <a href="/manifests/validate">validated manifest</a>; rotate or revoke credentials any time.</li>
</ol>
</ol>
<h2>Reference</h2>
<h2>Reference</h2>
-<p>Generated from <code>openvibe-contracts v${docs.contractsVersion}</code> and <code>openvibe-sdk v${docs.sdkVersion}</code>: <a href="/docs/contracts">contracts</a> · <a href="/docs/capabilities">capabilities</a> · <a href="/docs/events">events</a> · <a href="/docs/services">services</a> · <a href="/docs/tools">tools</a> · <a href="/docs/sdk">SDK</a>.</p>
+<p>Generated from <code>openvibe-contracts v${docs.contractsVersion}</code> and <code>openvibe-sdk v${docs.sdkVersion}</code>: <a href="/docs/api">API explorer</a> · <a href="/docs/contracts">contracts</a> · <a href="/docs/capabilities">capabilities</a> · <a href="/docs/events">events</a> · <a href="/docs/services">services</a> · <a href="/docs/tools">tools</a> · <a href="/docs/sdk">SDK</a>.</p>
<h2>Recent releases</h2>
<h2>Recent releases</h2>
${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((x) => [
${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((x) => [
html`<a href="/apps/${x.app_id}">${x.name}</a>`, x.kind, html`<a href="/releases/${x.id}">${x.version}</a>`, x.status, x.trust.tier, time(x.published_at),
html`<a href="/apps/${x.app_id}">${x.name}</a>`, x.kind, html`<a href="/releases/${x.id}">${x.version}</a>`, x.status, x.trust.tier, time(x.published_at),
@@ -271,10 +271,11 @@ ${signedIn ? releaseActions(req, rel) : ''}
@@ -271,10 +271,11 @@ ${signedIn ? releaseActions(req, rel) : ''}
].join('\n'));
].join('\n'));
});
});
r.get('/sitemap.xml', (req, res) => {
r.get('/sitemap.xml', (req, res) => {
- const urls = ['/', '/docs', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate',
+ const urls = ['/', '/docs', '/docs/api', '/docs/contracts', '/docs/capabilities', '/docs/events', '/docs/services', '/docs/tools', '/docs/sdk', '/oauth', '/tools/webhooks', '/manifests/validate',
'/policy', '/policy/rfc', '/policy/compatibility', '/policy/licensing', '/policy/transparency',
'/policy', '/policy/rfc', '/policy/compatibility', '/policy/licensing', '/policy/transparency',
...governance.filter((g) => !g.draft).map((g) => `/policy/${g.slug}`),
...governance.filter((g) => !g.draft).map((g) => `/policy/${g.slug}`),
...docs.contracts.map((c) => `/docs/contracts/${c.id}`), ...docs.capabilities.map((c) => `/docs/capabilities/${c.id}`),
...docs.contracts.map((c) => `/docs/contracts/${c.id}`), ...docs.capabilities.map((c) => `/docs/capabilities/${c.id}`),
+ ...require('openvibe-contracts').openapi.index().map((x) => `/docs/api/${x.service}`),
...docs.sdk.map((m) => `/docs/sdk/${m.slug}`), ...docs.adrs.map((a) => `/docs/adr/${a.id}`)];
...docs.sdk.map((m) => `/docs/sdk/${m.slug}`), ...docs.adrs.map((a) => `/docs/adr/${a.id}`)];
const x = (s) => s.replace(/&/g, '&').replace(/</g, '<');
const x = (s) => s.replace(/&/g, '&').replace(/</g, '<');
res.set('Cache-Control', 'public, max-age=3600').type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => `<url><loc>${x(config.baseUrl + u)}</loc></url>`).join('\n')}\n</urlset>\n`);
res.set('Cache-Control', 'public, max-age=3600').type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => `<url><loc>${x(config.baseUrl + u)}</loc></url>`).join('\n')}\n</urlset>\n`);
@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -19,11 +19,11 @@ const sdkPkg = require('openvibe-sdk/package.json');
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
await check('package.json pins the contracts, SDK and shared tags that are installed', async () => {
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
const tag = (dep) => (pkg.dependencies[dep].match(/refs\/tags\/(v[0-9.]+)$/) || [])[1];
- assert.strictEqual(tag('openvibe-contracts'), 'v0.49.0');
+ assert.strictEqual(tag('openvibe-contracts'), 'v0.51.0');
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-sdk'), `v${sdkPkg.version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(tag('openvibe-shared'), `v${require('openvibe-shared/package.json').version}`);
assert.strictEqual(sdkPkg.version, '0.11.0');
assert.strictEqual(sdkPkg.version, '0.11.0');
- // The contracts tag v0.49.0 carries the released codes manifest and codes.app-manifest@1.
+ // The contracts tag v0.51.0 carries the released codes manifest and codes.app-manifest@1.
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.services.get('codes') && contracts.services.get('codes').status === 'alpha');
assert.ok(contracts.resolve('codes.app-manifest@1'));
assert.ok(contracts.resolve('codes.app-manifest@1'));
});
});
@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -33,13 +33,13 @@ const sdkPkg = require('openvibe-sdk/package.json');
const r = await t.get(p);
const r = await t.get(p);
assert.strictEqual(r.status, 200, p);
assert.strictEqual(r.status, 200, p);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
assert.ok(r.text.includes(`openvibe-contracts v${contractsVersion}`), `${p} names the installed contracts version`);
- assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.49.0'), `${p} links the pinned tag`);
- if (contractsVersion !== '0.49.0') assert.ok(r.text.includes('(tag v0.49.0)'), `${p} says the tag differs from the package version`);
+ assert.ok(r.text.includes('OpenVibe.Contracts/tree/v0.51.0'), `${p} links the pinned tag`);
+ if (contractsVersion !== '0.51.0') assert.ok(r.text.includes('(tag v0.51.0)'), `${p} says the tag differs from the package version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
assert.ok(r.text.includes(`openvibe-sdk v${sdkPkg.version}`), `${p} names the SDK version`);
}
}
const v = (await t.get('/api/v1/docs/versions')).json();
const v = (await t.get('/api/v1/docs/versions')).json();
assert.strictEqual(v.contracts, contractsVersion);
assert.strictEqual(v.contracts, contractsVersion);
- assert.strictEqual(v.contracts_tag, 'v0.49.0');
+ assert.strictEqual(v.contracts_tag, 'v0.51.0');
assert.strictEqual(v.sdk, sdkPkg.version);
assert.strictEqual(v.sdk, sdkPkg.version);
});
});
@@ -105,6 +105,35 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -105,6 +105,35 @@ const sdkPkg = require('openvibe-sdk/package.json');
assert.match(r.headers.get('cache-control'), /public/);
assert.match(r.headers.get('cache-control'), /public/);
const sm = await t.get('/sitemap.xml');
const sm = await t.get('/sitemap.xml');
assert.ok(sm.text.includes('/docs/contracts/mods.mod-manifest</loc>'));
assert.ok(sm.text.includes('/docs/contracts/mods.mod-manifest</loc>'));
+ assert.ok(sm.text.includes('/docs/api</loc>') && sm.text.includes('/docs/api/tools</loc>'), 'the API explorer is in the sitemap');
+ });
+
+ await check('the API explorer lists every service document from the pinned contracts (WS-C task 6)', async () => {
+ const index = contracts.openapi.index();
+ assert.ok(index.length >= 20);
+ const list = await t.get('/docs/api');
+ assert.strictEqual(list.status, 200);
+ for (const s of index) assert.ok(list.text.includes(`href="/docs/api/${s.service}"`), `${s.service} is listed`);
+ assert.ok((await t.get('/docs')).text.includes('href="/docs/api"'), 'the docs index links it');
+ const raw = await t.get('/docs/api/tools.json');
+ assert.strictEqual(raw.status, 200);
+ assert.match(raw.headers.get('content-type'), /application\/vnd\.oai\.openapi\+json/);
+ assert.strictEqual(raw.headers.get('access-control-allow-origin'), '*');
+ assert.deepStrictEqual(raw.json(), contracts.openapi.document('tools'), 'the document as the package ships it');
+ const page = await t.get('/docs/api/tools');
+ assert.strictEqual(page.status, 200);
+ const doc = contracts.openapi.document('tools');
+ const ops = Object.values(doc.paths).reduce((n, m) => n + Object.keys(m).length, 0);
+ assert.strictEqual((page.text.match(/class="api-op"/g) || []).length, ops, 'one block per route');
+ assert.ok(page.text.includes('id="cap-tools.job.create"') && page.text.includes('href="/docs/capabilities/tools.job.create"'), 'capabilities anchored and linked');
+ assert.ok(page.text.includes('href="/docs/contracts/tools.job"'), 'schemas link to their contract pages');
+ assert.ok(page.text.includes('<code>/api/v1/jobs/{id}</code>'));
+ const pub = await t.get('/docs/api/tools?public=1');
+ assert.ok((pub.text.match(/class="api-op"/g) || []).length <= ops);
+ assert.strictEqual((await t.get('/docs/api/nope')).status, 404);
+ assert.strictEqual((await t.get('/docs/api/nope.json')).status, 404);
+ const cap = await t.get('/docs/capabilities/tools.job.create');
+ assert.ok(cap.text.includes('href="/docs/api/tools#cap-tools.job.create"'), 'a capability links its routes');
});
});
await check('pages rendered for a signed-in person are never publicly cacheable', async () => {
await check('pages rendered for a signed-in person are never publicly cacheable', async () => {