openvibers/openvibe.services Commits
Services: code blocks are keyboard-focusable (axe scrollable-region-focusable) (#72)
Changes
@@@ -65,7 +65,7 @@ function createDocsRoutes(ctx) {<li><a href="/policy/rfc"><strong>Decisions</strong></a><span>${docs.adrs.length} architecture decision records</span></li></ul><h2>Install</h2>--<pre><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}++<pre tabindex="0"><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}npm install https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/${docs.contractsTag}</code></pre><p>The SDK is ${docs.sdkLicense}; the contracts are ${docs.contractsLicense}. The SDK release is tested against contracts <code>${docs.sdkContractsRange || 'n/a'}</code>.</p>`,});@@@ -103,7 +103,7 @@ ${table(['Contract', 'Version', 'Owner', 'Visibility', 'Status', 'Compatibility'const c = docs.contract(req.params.id);if (!c) return next();const major = c.version.split('.')[0];-- const fixture = (f, ok) => html`<details${ok ? raw(' open') : ''}><summary>${ok ? 'Valid' : 'Rejected'}: ${f.name}</summary><pre><code>${JSON.stringify(f.body, null, 2)}</code></pre></details>`;++ const fixture = (f, ok) => html`<details${ok ? raw(' open') : ''}><summary>${ok ? 'Valid' : 'Rejected'}: ${f.name}</summary><pre tabindex="0"><code>${JSON.stringify(f.body, null, 2)}</code></pre></details>`;page(req, res, {title: `${c.id}@${major}`,description: c.description.slice(0, 200),@@@ -127,7 +127,7 @@ ${table(['Field', 'Type', 'Required', 'Description', 'Constraints'], c.fields.ma${c.fixtures.valid.map((f) => fixture(f, true))}${c.fixtures.invalid.map((f) => fixture(f, false))}${!c.fixtures.valid.length && !c.fixtures.invalid.length ? html`<p class="muted">No fixtures ship with this contract.</p>` : ''}<h2>Validate</h2>--<pre><code>const contracts = require('openvibe-contracts');++<pre tabindex="0"><code>const contracts = require('openvibe-contracts');contracts.validate('${c.id}@${major}', value); // { valid, errors: [{ path, message }] }</code></pre>`,});});@@@ -276,25 +276,25 @@ ${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><h2>Where entries come from</h2><p>OpenVibe.Blog watches every running service's release: the <code>release</code> in its <code>/release.json</code>, as OpenVibe.Network's registry reports it. When a release changes, GitHub's compare of the old and new commit gives the commits that shipped. Each one becomes an entry, with the commit message as its text, and batches of them are published as "Patch notes" posts. A service appears once it serves <code>/release.json</code> and its registry entry names its repository.</p><h2>The feed</h2>--<pre><code>GET https://openvibe.network/api/v1/changelog?service=<id>&limit=<1-100>&before=<cursor>++<pre tabindex="0"><code>GET https://openvibe.network/api/v1/changelog?service=<id>&limit=<1-100>&before=<cursor>→ { service, entries: [{ service, sha, short, subject, author, committed_at, deployed_at, major, url, post_id }],latest_post: { id, title, url, published_at, entries } | null, posts, next }</code></pre><p>It is public, CORS <code>*</code> and cached for 60 s (stale while the blog is down). Leave <code>service</code> out for the whole network.</p>${sample ? html`<details open><summary>Live sample: the last ${String((sample.entries || []).length)} entries for this site</summary><ul>${(sample.entries || []).map((e) => html`<li><a href="${e.url}"><code>${e.short}</code></a> ${e.subject} ${e.deployed_at ? html`<span class="muted small">(${time(e.deployed_at)})</span>` : ''}</li>`)}</ul></details>`: html`<p class="muted">The feed could not be read just now (${failed}); the shape above is what it answers.</p>`}<h2>On a server-rendered site</h2>--<pre><code>const frame = require('openvibe-shared/frame');++<pre tabindex="0"><code>const frame = require('openvibe-shared/frame');frame.shipped({ service: 'myservice', title: 'Recently shipped on MySite' }) // home: pill + recent listframe.updatesBody({ service: 'myservice', siteName: 'MySite' }) // the body of your /updates pageframe.shippedScript() // the <script> that fills them</code></pre><p>The footer from <code>openvibe-shared/footer</code> already carries the "shipped X ago" line and an Updates link.</p><h2>The markup, if you render it yourself</h2>--<pre><code><a data-ov-shipped="latest" data-service="myservice" href="/updates" hidden></a>++<pre tabindex="0"><code><a data-ov-shipped="latest" data-service="myservice" href="/updates" hidden></a><div data-ov-shipped="list" data-service="myservice" data-limit="5" data-more="/updates" data-title="Recently shipped" hidden></div><div data-ov-shipped="log" data-service="myservice" data-limit="50"></div></code></pre><p><code>shipped.js</code> (served by every site from its pinned openvibe-shared) mounts every <code>[data-ov-shipped]</code> element: <code>latest</code> is the pill, <code>list</code> the recent changes, and <code>log</code> the full log with days, "Load more" and the Patch notes posts. Elements stay hidden until there is something to show.</p><h2>In a browser app outside the network</h2>--<pre><code>import { mountFrame } from 'openvibe-sdk/frame';++<pre tabindex="0"><code>import { mountFrame } from 'openvibe-sdk/frame';await mountFrame({ service: 'myapp' }); // navbar and footer, with the shipped lineawait mountFrame({ service: 'myapp', shipped: false }); // without it</code></pre><p>Live's <a href="https://openvibe.live/updates">/updates</a>, <a href="/updates">this site's</a> and <a href="https://openvibe.network/updates">the whole network's</a> are the same component.</p>`,@@@ -516,7 +516,7 @@ ${table(['Module', 'Where', 'Declarations', 'Types file'], docs.sdk.map((m) => [body: html`<h1><code>${m.name}</code></h1>${versions()}<p class="muted">From <code>${m.typesFile}</code> (${m.browser}). Declarations are shown verbatim.</p><nav class="toc"><ul class="plain">${m.declarations.map((d, i) => html`<li><a href="#d${i}">${d.kind} ${d.name}</a></li>`)}</ul></nav>--${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>${d.kind}</small> ${d.name}</h2>${d.doc ? html`<p>${d.doc}</p>` : ''}<pre><code>${d.text}</code></pre></section>`)}`,++${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>${d.kind}</small> ${d.name}</h2>${d.doc ? html`<p>${d.doc}</p>` : ''}<pre tabindex="0"><code>${d.text}</code></pre></section>`)}`,});});@@@ -81,7 +81,7 @@ const services = await ov.registry.services();`;lede: 'One console for everything programmable on OpenVibe: projects, scoped credentials, capability grants, usage and releases, over every service. The reference is generated from the exact contract and SDK versions production runs.',actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Read the docs', href: '/docs' }],note: 'Open source. Network owns projects, apps, credentials, grants and quotas; this console shows its API with your own sign-in and never stores a secret.',-- aside: { html: `<pre class="services-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },++ aside: { html: `<pre tabindex="0" class="services-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },}))}<div class="notice">Status: alpha. What works and what does not is listed on the <a href="/policy/transparency">transparency page</a>.</div>${raw(showcase.features({@@@ -262,7 +262,7 @@ ${req.query.done ? notice(`Release ${req.query.done}.`, 'ok') : ''}${rel.deprecated_at ? html`<dt>Deprecated</dt><dd>${time(rel.deprecated_at)}: ${rel.deprecation_reason}${rel.replacement ? html` — use <a href="/releases/${rel.replacement}">${rel.replacement}</a>` : ''}</dd>` : ''}${rel.revoked_at ? html`<dt>Revoked</dt><dd>${time(rel.revoked_at)}: ${rel.revocation_reason}</dd>` : ''}</dl>${rel.notes ? html`<h2>Notes</h2><p>${rel.notes}</p>` : ''}--<h2>Manifest</h2><p class="muted small">Validated with openvibe-contracts ${m ? m.contracts_version : '?'}.</p><pre><code>${m ? JSON.stringify(m.body, null, 2) : ''}</code></pre>++<h2>Manifest</h2><p class="muted small">Validated with openvibe-contracts ${m ? m.contracts_version : '?'}.</p><pre tabindex="0"><code>${m ? JSON.stringify(m.body, null, 2) : ''}</code></pre>${signedIn ? releaseActions(req, rel) : ''}<h2>History</h2>${table(['When', 'Action', 'By'], (await releases.log(rel.id)).map((l) => [time(l.at), l.action, l.actor]))}`,});@@@ -677,7 +677,7 @@ ${admin ? html`<h2>Set a secret</h2><div class="notice warn"><strong>You won't see this secret again.</strong> Copy it into your server's secret store now. Services does not keep it and OpenVibe.Network stores only a hash (credential <code>${credential.id}</code>, ending <code>…${credential.hint}</code>). If you lose it, rotate the credential. Never put it in a browser or a repository.</div>${rotated && previous.length ? html`<p>The previous secret${previous.length > 1 ? 's stay' : ' stays'} valid until ${previous.map((p) => html`${time(p.expires_at)} `)} so you can deploy the new one first.</p>` : ''}<h2>Use it</h2>--<pre><code>const sdk = require('openvibe-sdk');++<pre tabindex="0"><code>const sdk = require('openvibe-sdk');const client = sdk.createClient({tokenProvider: sdk.auth.createServiceTokenClient({ clientId: '${app.id}', clientSecret: process.env.OV_CLIENT_SECRET }),});</code></pre>@@@ -909,7 +909,7 @@ ${table(['When', 'Playground', 'Credential', 'Outcome', 'Stage', 'HTTP', 'Code',function runResult(r) {return html`<div class="result ${r.outcome === 'ok' ? 'ok' : 'bad'}" role="status"><p><strong>${r.outcome === 'ok' ? 'Done.' : (r.outcome === 'refused' ? 'Refused.' : 'Failed.')}</strong> ${r.detail}</p><p class="muted small">stage ${r.stage}${r.http_status ? ` · HTTP ${r.http_status}` : ''}${r.code ? html` · <code>${r.code}</code>` : ''}${r.runId ? html` · run <code>${r.runId}</code>` : ''}</p>--${r.result ? html`<pre><code>${JSON.stringify(r.result, null, 2)}</code></pre>` : ''}</div>`;++${r.result ? html`<pre tabindex="0"><code>${JSON.stringify(r.result, null, 2)}</code></pre>` : ''}</div>`;}const statusFor = (out) => (out.outcome === 'ok' ? 200 : (out.stage === 'grant' || out.stage === 'app' ? 403 : (out.stage === 'input' ? 422 : (out.stage === 'rate' ? 429 : (out.stage === 'token' ? (out.http_status && out.http_status >= 400 ? out.http_status : 401) : 502)))));@@@ -79,7 +79,7 @@ ${table(['Parameter', 'Value'], Object.entries(got).filter(([, v]) => v !== null${got.error ? notice(html`Network returned <code>${got.error}</code>${got.error_description ? html`: ${got.error_description}` : ''}. <code>access_denied</code> means the person declined; <code>interaction_required</code> means <code>prompt=none</code> was sent (apps must not).`, 'warn') : ''}${got.code ? html`<h2>Exchange it (on your server)</h2><p>Send <code>client_secret</code> only for a confidential app, and only from a server — never from a browser.</p>--<pre><code>curl -X POST ${config.network.url}/oauth/token \\++<pre tabindex="0"><code>curl -X POST ${config.network.url}/oauth/token \\-d grant_type=authorization_code \\-d client_id="$CLIENT_ID" \\-d client_secret="$CLIENT_SECRET" \\@@@ -117,8 +117,8 @@ ${got.code ? html`<h2>Exchange it (on your server)</h2><button type="submit">Generate</button></form>${sample ? html`<div class="result"><p>A delivery exactly as Events would send it (event types from <code>openvibe-contracts v${docs.contractsVersion}</code>). The envelope ${sample.envelopeValid ? 'validates' : 'does NOT validate'} as events.event-envelope@1. The payload is empty: contracts define no payload schema for this event type yet, and Services does not invent one.</p>${table(['Header', 'Value'], Object.entries(sample.headers).map(([k, v]) => [code(k), code(v)]))}--<p>Body (byte for byte):</p><pre><code>${sample.body}</code></pre>--<p>Replay it against your own endpoint (Services never sends requests to your URLs). The v2 timestamp is the time you pressed Generate, so a receiver refuses the replay once it is more than ${webhooks.V2_WINDOW_SEC} seconds old: generate a fresh one, or use it to check that your endpoint does refuse a stale delivery.</p><pre><code>${webhooks.curlFor(sample.headers, sample.body)}</code></pre></div>` : ''}`,++<p>Body (byte for byte):</p><pre tabindex="0"><code>${sample.body}</code></pre>++<p>Replay it against your own endpoint (Services never sends requests to your URLs). The v2 timestamp is the time you pressed Generate, so a receiver refuses the replay once it is more than ${webhooks.V2_WINDOW_SEC} seconds old: generate a fresh one, or use it to check that your endpoint does refuse a stale delivery.</p><pre tabindex="0"><code>${webhooks.curlFor(sample.headers, sample.body)}</code></pre></div>` : ''}`,});function verifyResult(v) {@@@ -30,7 +30,7 @@ function markdown(src, { headingOffset = 1 } = {}) {const flushList = () => { if (list) { out.push(`<${list.type}>${list.items.map((i) => `<li>${inline(i)}</li>`).join('')}</${list.type}>`); list = null; } };for (const line of lines) {if (fence) {-- if (/^```/.test(line)) { out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);++ if (/^```/.test(line)) { out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);continue;}if (/^```/.test(line)) { flushPara(); flushList(); fence = []; continue; }@@@ -54,7 +54,7 @@ function markdown(src, { headingOffset = 1 } = {}) {flushList();para.push(line.trim());}-- if (fence) out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`);++ if (fence) out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`);flushPara(); flushList();return out.join('\n');}@@@ -120,6 +120,8 @@ const sdkPkg = require('openvibe-sdk/package.json');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');++ // Code blocks scroll sideways on a phone, so each is a keyboard stop (axe scrollable-region-focusable).++ assert.ok(!/<pre(?![^>]*tabindex)[\s>]/.test((await t.get('/docs')).text), 'every <pre> on /docs is focusable');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/);
@@ -65,7 +65,7 @@ function createDocsRoutes(ctx) {
@@ -65,7 +65,7 @@ function createDocsRoutes(ctx) {
<li><a href="/policy/rfc"><strong>Decisions</strong></a><span>${docs.adrs.length} architecture decision records</span></li>
<li><a href="/policy/rfc"><strong>Decisions</strong></a><span>${docs.adrs.length} architecture decision records</span></li>
</ul>
</ul>
<h2>Install</h2>
<h2>Install</h2>
-<pre><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}
+<pre tabindex="0"><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}
npm install https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/${docs.contractsTag}</code></pre>
npm install https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/${docs.contractsTag}</code></pre>
<p>The SDK is ${docs.sdkLicense}; the contracts are ${docs.contractsLicense}. The SDK release is tested against contracts <code>${docs.sdkContractsRange || 'n/a'}</code>.</p>`,
<p>The SDK is ${docs.sdkLicense}; the contracts are ${docs.contractsLicense}. The SDK release is tested against contracts <code>${docs.sdkContractsRange || 'n/a'}</code>.</p>`,
});
});
@@ -103,7 +103,7 @@ ${table(['Contract', 'Version', 'Owner', 'Visibility', 'Status', 'Compatibility'
@@ -103,7 +103,7 @@ ${table(['Contract', 'Version', 'Owner', 'Visibility', 'Status', 'Compatibility'
const c = docs.contract(req.params.id);
const c = docs.contract(req.params.id);
if (!c) return next();
if (!c) return next();
const major = c.version.split('.')[0];
const major = c.version.split('.')[0];
- const fixture = (f, ok) => html`<details${ok ? raw(' open') : ''}><summary>${ok ? 'Valid' : 'Rejected'}: ${f.name}</summary><pre><code>${JSON.stringify(f.body, null, 2)}</code></pre></details>`;
+ const fixture = (f, ok) => html`<details${ok ? raw(' open') : ''}><summary>${ok ? 'Valid' : 'Rejected'}: ${f.name}</summary><pre tabindex="0"><code>${JSON.stringify(f.body, null, 2)}</code></pre></details>`;
page(req, res, {
page(req, res, {
title: `${c.id}@${major}`,
title: `${c.id}@${major}`,
description: c.description.slice(0, 200),
description: c.description.slice(0, 200),
@@ -127,7 +127,7 @@ ${table(['Field', 'Type', 'Required', 'Description', 'Constraints'], c.fields.ma
@@ -127,7 +127,7 @@ ${table(['Field', 'Type', 'Required', 'Description', 'Constraints'], c.fields.ma
${c.fixtures.valid.map((f) => fixture(f, true))}${c.fixtures.invalid.map((f) => fixture(f, false))}
${c.fixtures.valid.map((f) => fixture(f, true))}${c.fixtures.invalid.map((f) => fixture(f, false))}
${!c.fixtures.valid.length && !c.fixtures.invalid.length ? html`<p class="muted">No fixtures ship with this contract.</p>` : ''}
${!c.fixtures.valid.length && !c.fixtures.invalid.length ? html`<p class="muted">No fixtures ship with this contract.</p>` : ''}
<h2>Validate</h2>
<h2>Validate</h2>
-<pre><code>const contracts = require('openvibe-contracts');
+<pre tabindex="0"><code>const contracts = require('openvibe-contracts');
contracts.validate('${c.id}@${major}', value); // { valid, errors: [{ path, message }] }</code></pre>`,
contracts.validate('${c.id}@${major}', value); // { valid, errors: [{ path, message }] }</code></pre>`,
});
});
});
});
@@ -276,25 +276,25 @@ ${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2>
@@ -276,25 +276,25 @@ ${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2>
<h2>Where entries come from</h2>
<h2>Where entries come from</h2>
<p>OpenVibe.Blog watches every running service's release: the <code>release</code> in its <code>/release.json</code>, as OpenVibe.Network's registry reports it. When a release changes, GitHub's compare of the old and new commit gives the commits that shipped. Each one becomes an entry, with the commit message as its text, and batches of them are published as "Patch notes" posts. A service appears once it serves <code>/release.json</code> and its registry entry names its repository.</p>
<p>OpenVibe.Blog watches every running service's release: the <code>release</code> in its <code>/release.json</code>, as OpenVibe.Network's registry reports it. When a release changes, GitHub's compare of the old and new commit gives the commits that shipped. Each one becomes an entry, with the commit message as its text, and batches of them are published as "Patch notes" posts. A service appears once it serves <code>/release.json</code> and its registry entry names its repository.</p>
<h2>The feed</h2>
<h2>The feed</h2>
-<pre><code>GET https://openvibe.network/api/v1/changelog?service=<id>&limit=<1-100>&before=<cursor>
+<pre tabindex="0"><code>GET https://openvibe.network/api/v1/changelog?service=<id>&limit=<1-100>&before=<cursor>
→ { service, entries: [{ service, sha, short, subject, author, committed_at, deployed_at, major, url, post_id }],
→ { service, entries: [{ service, sha, short, subject, author, committed_at, deployed_at, major, url, post_id }],
latest_post: { id, title, url, published_at, entries } | null, posts, next }</code></pre>
latest_post: { id, title, url, published_at, entries } | null, posts, next }</code></pre>
<p>It is public, CORS <code>*</code> and cached for 60 s (stale while the blog is down). Leave <code>service</code> out for the whole network.</p>
<p>It is public, CORS <code>*</code> and cached for 60 s (stale while the blog is down). Leave <code>service</code> out for the whole network.</p>
${sample ? html`<details open><summary>Live sample: the last ${String((sample.entries || []).length)} entries for this site</summary><ul>${(sample.entries || []).map((e) => html`<li><a href="${e.url}"><code>${e.short}</code></a> ${e.subject} ${e.deployed_at ? html`<span class="muted small">(${time(e.deployed_at)})</span>` : ''}</li>`)}</ul></details>`
${sample ? html`<details open><summary>Live sample: the last ${String((sample.entries || []).length)} entries for this site</summary><ul>${(sample.entries || []).map((e) => html`<li><a href="${e.url}"><code>${e.short}</code></a> ${e.subject} ${e.deployed_at ? html`<span class="muted small">(${time(e.deployed_at)})</span>` : ''}</li>`)}</ul></details>`
: html`<p class="muted">The feed could not be read just now (${failed}); the shape above is what it answers.</p>`}
: html`<p class="muted">The feed could not be read just now (${failed}); the shape above is what it answers.</p>`}
<h2>On a server-rendered site</h2>
<h2>On a server-rendered site</h2>
-<pre><code>const frame = require('openvibe-shared/frame');
+<pre tabindex="0"><code>const frame = require('openvibe-shared/frame');
frame.shipped({ service: 'myservice', title: 'Recently shipped on MySite' }) // home: pill + recent list
frame.shipped({ service: 'myservice', title: 'Recently shipped on MySite' }) // home: pill + recent list
frame.updatesBody({ service: 'myservice', siteName: 'MySite' }) // the body of your /updates page
frame.updatesBody({ service: 'myservice', siteName: 'MySite' }) // the body of your /updates page
frame.shippedScript() // the <script> that fills them</code></pre>
frame.shippedScript() // the <script> that fills them</code></pre>
<p>The footer from <code>openvibe-shared/footer</code> already carries the "shipped X ago" line and an Updates link.</p>
<p>The footer from <code>openvibe-shared/footer</code> already carries the "shipped X ago" line and an Updates link.</p>
<h2>The markup, if you render it yourself</h2>
<h2>The markup, if you render it yourself</h2>
-<pre><code><a data-ov-shipped="latest" data-service="myservice" href="/updates" hidden></a>
+<pre tabindex="0"><code><a data-ov-shipped="latest" data-service="myservice" href="/updates" hidden></a>
<div data-ov-shipped="list" data-service="myservice" data-limit="5" data-more="/updates" data-title="Recently shipped" hidden></div>
<div data-ov-shipped="list" data-service="myservice" data-limit="5" data-more="/updates" data-title="Recently shipped" hidden></div>
<div data-ov-shipped="log" data-service="myservice" data-limit="50"></div></code></pre>
<div data-ov-shipped="log" data-service="myservice" data-limit="50"></div></code></pre>
<p><code>shipped.js</code> (served by every site from its pinned openvibe-shared) mounts every <code>[data-ov-shipped]</code> element: <code>latest</code> is the pill, <code>list</code> the recent changes, and <code>log</code> the full log with days, "Load more" and the Patch notes posts. Elements stay hidden until there is something to show.</p>
<p><code>shipped.js</code> (served by every site from its pinned openvibe-shared) mounts every <code>[data-ov-shipped]</code> element: <code>latest</code> is the pill, <code>list</code> the recent changes, and <code>log</code> the full log with days, "Load more" and the Patch notes posts. Elements stay hidden until there is something to show.</p>
<h2>In a browser app outside the network</h2>
<h2>In a browser app outside the network</h2>
-<pre><code>import { mountFrame } from 'openvibe-sdk/frame';
+<pre tabindex="0"><code>import { mountFrame } from 'openvibe-sdk/frame';
await mountFrame({ service: 'myapp' }); // navbar and footer, with the shipped line
await mountFrame({ service: 'myapp' }); // navbar and footer, with the shipped line
await mountFrame({ service: 'myapp', shipped: false }); // without it</code></pre>
await mountFrame({ service: 'myapp', shipped: false }); // without it</code></pre>
<p>Live's <a href="https://openvibe.live/updates">/updates</a>, <a href="/updates">this site's</a> and <a href="https://openvibe.network/updates">the whole network's</a> are the same component.</p>`,
<p>Live's <a href="https://openvibe.live/updates">/updates</a>, <a href="/updates">this site's</a> and <a href="https://openvibe.network/updates">the whole network's</a> are the same component.</p>`,
@@ -516,7 +516,7 @@ ${table(['Module', 'Where', 'Declarations', 'Types file'], docs.sdk.map((m) => [
@@ -516,7 +516,7 @@ ${table(['Module', 'Where', 'Declarations', 'Types file'], docs.sdk.map((m) => [
body: html`<h1><code>${m.name}</code></h1>${versions()}
body: html`<h1><code>${m.name}</code></h1>${versions()}
<p class="muted">From <code>${m.typesFile}</code> (${m.browser}). Declarations are shown verbatim.</p>
<p class="muted">From <code>${m.typesFile}</code> (${m.browser}). Declarations are shown verbatim.</p>
<nav class="toc"><ul class="plain">${m.declarations.map((d, i) => html`<li><a href="#d${i}">${d.kind} ${d.name}</a></li>`)}</ul></nav>
<nav class="toc"><ul class="plain">${m.declarations.map((d, i) => html`<li><a href="#d${i}">${d.kind} ${d.name}</a></li>`)}</ul></nav>
-${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>${d.kind}</small> ${d.name}</h2>${d.doc ? html`<p>${d.doc}</p>` : ''}<pre><code>${d.text}</code></pre></section>`)}`,
+${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>${d.kind}</small> ${d.name}</h2>${d.doc ? html`<p>${d.doc}</p>` : ''}<pre tabindex="0"><code>${d.text}</code></pre></section>`)}`,
});
});
});
});
@@ -81,7 +81,7 @@ const services = await ov.registry.services();`;
@@ -81,7 +81,7 @@ const services = await ov.registry.services();`;
lede: 'One console for everything programmable on OpenVibe: projects, scoped credentials, capability grants, usage and releases, over every service. The reference is generated from the exact contract and SDK versions production runs.',
lede: 'One console for everything programmable on OpenVibe: projects, scoped credentials, capability grants, usage and releases, over every service. The reference is generated from the exact contract and SDK versions production runs.',
actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Read the docs', href: '/docs' }],
actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Read the docs', href: '/docs' }],
note: 'Open source. Network owns projects, apps, credentials, grants and quotas; this console shows its API with your own sign-in and never stores a secret.',
note: 'Open source. Network owns projects, apps, credentials, grants and quotas; this console shows its API with your own sign-in and never stores a secret.',
- aside: { html: `<pre class="services-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },
+ aside: { html: `<pre tabindex="0" class="services-hero-code"><code>${showcase.esc(snippet)}</code></pre>` },
}))}
}))}
<div class="notice">Status: alpha. What works and what does not is listed on the <a href="/policy/transparency">transparency page</a>.</div>
<div class="notice">Status: alpha. What works and what does not is listed on the <a href="/policy/transparency">transparency page</a>.</div>
${raw(showcase.features({
${raw(showcase.features({
@@ -262,7 +262,7 @@ ${req.query.done ? notice(`Release ${req.query.done}.`, 'ok') : ''}
@@ -262,7 +262,7 @@ ${req.query.done ? notice(`Release ${req.query.done}.`, 'ok') : ''}
${rel.deprecated_at ? html`<dt>Deprecated</dt><dd>${time(rel.deprecated_at)}: ${rel.deprecation_reason}${rel.replacement ? html` — use <a href="/releases/${rel.replacement}">${rel.replacement}</a>` : ''}</dd>` : ''}
${rel.deprecated_at ? html`<dt>Deprecated</dt><dd>${time(rel.deprecated_at)}: ${rel.deprecation_reason}${rel.replacement ? html` — use <a href="/releases/${rel.replacement}">${rel.replacement}</a>` : ''}</dd>` : ''}
${rel.revoked_at ? html`<dt>Revoked</dt><dd>${time(rel.revoked_at)}: ${rel.revocation_reason}</dd>` : ''}</dl>
${rel.revoked_at ? html`<dt>Revoked</dt><dd>${time(rel.revoked_at)}: ${rel.revocation_reason}</dd>` : ''}</dl>
${rel.notes ? html`<h2>Notes</h2><p>${rel.notes}</p>` : ''}
${rel.notes ? html`<h2>Notes</h2><p>${rel.notes}</p>` : ''}
-<h2>Manifest</h2><p class="muted small">Validated with openvibe-contracts ${m ? m.contracts_version : '?'}.</p><pre><code>${m ? JSON.stringify(m.body, null, 2) : ''}</code></pre>
+<h2>Manifest</h2><p class="muted small">Validated with openvibe-contracts ${m ? m.contracts_version : '?'}.</p><pre tabindex="0"><code>${m ? JSON.stringify(m.body, null, 2) : ''}</code></pre>
${signedIn ? releaseActions(req, rel) : ''}
${signedIn ? releaseActions(req, rel) : ''}
<h2>History</h2>${table(['When', 'Action', 'By'], (await releases.log(rel.id)).map((l) => [time(l.at), l.action, l.actor]))}`,
<h2>History</h2>${table(['When', 'Action', 'By'], (await releases.log(rel.id)).map((l) => [time(l.at), l.action, l.actor]))}`,
});
});
@@ -677,7 +677,7 @@ ${admin ? html`<h2>Set a secret</h2>
@@ -677,7 +677,7 @@ ${admin ? html`<h2>Set a secret</h2>
<div class="notice warn"><strong>You won't see this secret again.</strong> Copy it into your server's secret store now. Services does not keep it and OpenVibe.Network stores only a hash (credential <code>${credential.id}</code>, ending <code>…${credential.hint}</code>). If you lose it, rotate the credential. Never put it in a browser or a repository.</div>
<div class="notice warn"><strong>You won't see this secret again.</strong> Copy it into your server's secret store now. Services does not keep it and OpenVibe.Network stores only a hash (credential <code>${credential.id}</code>, ending <code>…${credential.hint}</code>). If you lose it, rotate the credential. Never put it in a browser or a repository.</div>
${rotated && previous.length ? html`<p>The previous secret${previous.length > 1 ? 's stay' : ' stays'} valid until ${previous.map((p) => html`${time(p.expires_at)} `)} so you can deploy the new one first.</p>` : ''}
${rotated && previous.length ? html`<p>The previous secret${previous.length > 1 ? 's stay' : ' stays'} valid until ${previous.map((p) => html`${time(p.expires_at)} `)} so you can deploy the new one first.</p>` : ''}
<h2>Use it</h2>
<h2>Use it</h2>
-<pre><code>const sdk = require('openvibe-sdk');
+<pre tabindex="0"><code>const sdk = require('openvibe-sdk');
const client = sdk.createClient({
const client = sdk.createClient({
tokenProvider: sdk.auth.createServiceTokenClient({ clientId: '${app.id}', clientSecret: process.env.OV_CLIENT_SECRET }),
tokenProvider: sdk.auth.createServiceTokenClient({ clientId: '${app.id}', clientSecret: process.env.OV_CLIENT_SECRET }),
});</code></pre>
});</code></pre>
@@ -909,7 +909,7 @@ ${table(['When', 'Playground', 'Credential', 'Outcome', 'Stage', 'HTTP', 'Code',
@@ -909,7 +909,7 @@ ${table(['When', 'Playground', 'Credential', 'Outcome', 'Stage', 'HTTP', 'Code',
function runResult(r) {
function runResult(r) {
return html`<div class="result ${r.outcome === 'ok' ? 'ok' : 'bad'}" role="status"><p><strong>${r.outcome === 'ok' ? 'Done.' : (r.outcome === 'refused' ? 'Refused.' : 'Failed.')}</strong> ${r.detail}</p>
return html`<div class="result ${r.outcome === 'ok' ? 'ok' : 'bad'}" role="status"><p><strong>${r.outcome === 'ok' ? 'Done.' : (r.outcome === 'refused' ? 'Refused.' : 'Failed.')}</strong> ${r.detail}</p>
<p class="muted small">stage ${r.stage}${r.http_status ? ` · HTTP ${r.http_status}` : ''}${r.code ? html` · <code>${r.code}</code>` : ''}${r.runId ? html` · run <code>${r.runId}</code>` : ''}</p>
<p class="muted small">stage ${r.stage}${r.http_status ? ` · HTTP ${r.http_status}` : ''}${r.code ? html` · <code>${r.code}</code>` : ''}${r.runId ? html` · run <code>${r.runId}</code>` : ''}</p>
-${r.result ? html`<pre><code>${JSON.stringify(r.result, null, 2)}</code></pre>` : ''}</div>`;
+${r.result ? html`<pre tabindex="0"><code>${JSON.stringify(r.result, null, 2)}</code></pre>` : ''}</div>`;
}
}
const statusFor = (out) => (out.outcome === 'ok' ? 200 : (out.stage === 'grant' || out.stage === 'app' ? 403 : (out.stage === 'input' ? 422 : (out.stage === 'rate' ? 429 : (out.stage === 'token' ? (out.http_status && out.http_status >= 400 ? out.http_status : 401) : 502)))));
const statusFor = (out) => (out.outcome === 'ok' ? 200 : (out.stage === 'grant' || out.stage === 'app' ? 403 : (out.stage === 'input' ? 422 : (out.stage === 'rate' ? 429 : (out.stage === 'token' ? (out.http_status && out.http_status >= 400 ? out.http_status : 401) : 502)))));
@@ -79,7 +79,7 @@ ${table(['Parameter', 'Value'], Object.entries(got).filter(([, v]) => v !== null
@@ -79,7 +79,7 @@ ${table(['Parameter', 'Value'], Object.entries(got).filter(([, v]) => v !== null
${got.error ? notice(html`Network returned <code>${got.error}</code>${got.error_description ? html`: ${got.error_description}` : ''}. <code>access_denied</code> means the person declined; <code>interaction_required</code> means <code>prompt=none</code> was sent (apps must not).`, 'warn') : ''}
${got.error ? notice(html`Network returned <code>${got.error}</code>${got.error_description ? html`: ${got.error_description}` : ''}. <code>access_denied</code> means the person declined; <code>interaction_required</code> means <code>prompt=none</code> was sent (apps must not).`, 'warn') : ''}
${got.code ? html`<h2>Exchange it (on your server)</h2>
${got.code ? html`<h2>Exchange it (on your server)</h2>
<p>Send <code>client_secret</code> only for a confidential app, and only from a server — never from a browser.</p>
<p>Send <code>client_secret</code> only for a confidential app, and only from a server — never from a browser.</p>
-<pre><code>curl -X POST ${config.network.url}/oauth/token \\
+<pre tabindex="0"><code>curl -X POST ${config.network.url}/oauth/token \\
-d grant_type=authorization_code \\
-d grant_type=authorization_code \\
-d client_id="$CLIENT_ID" \\
-d client_id="$CLIENT_ID" \\
-d client_secret="$CLIENT_SECRET" \\
-d client_secret="$CLIENT_SECRET" \\
@@ -117,8 +117,8 @@ ${got.code ? html`<h2>Exchange it (on your server)</h2>
@@ -117,8 +117,8 @@ ${got.code ? html`<h2>Exchange it (on your server)</h2>
<button type="submit">Generate</button></form>
<button type="submit">Generate</button></form>
${sample ? html`<div class="result"><p>A delivery exactly as Events would send it (event types from <code>openvibe-contracts v${docs.contractsVersion}</code>). The envelope ${sample.envelopeValid ? 'validates' : 'does NOT validate'} as events.event-envelope@1. The payload is empty: contracts define no payload schema for this event type yet, and Services does not invent one.</p>
${sample ? html`<div class="result"><p>A delivery exactly as Events would send it (event types from <code>openvibe-contracts v${docs.contractsVersion}</code>). The envelope ${sample.envelopeValid ? 'validates' : 'does NOT validate'} as events.event-envelope@1. The payload is empty: contracts define no payload schema for this event type yet, and Services does not invent one.</p>
${table(['Header', 'Value'], Object.entries(sample.headers).map(([k, v]) => [code(k), code(v)]))}
${table(['Header', 'Value'], Object.entries(sample.headers).map(([k, v]) => [code(k), code(v)]))}
-<p>Body (byte for byte):</p><pre><code>${sample.body}</code></pre>
-<p>Replay it against your own endpoint (Services never sends requests to your URLs). The v2 timestamp is the time you pressed Generate, so a receiver refuses the replay once it is more than ${webhooks.V2_WINDOW_SEC} seconds old: generate a fresh one, or use it to check that your endpoint does refuse a stale delivery.</p><pre><code>${webhooks.curlFor(sample.headers, sample.body)}</code></pre></div>` : ''}`,
+<p>Body (byte for byte):</p><pre tabindex="0"><code>${sample.body}</code></pre>
+<p>Replay it against your own endpoint (Services never sends requests to your URLs). The v2 timestamp is the time you pressed Generate, so a receiver refuses the replay once it is more than ${webhooks.V2_WINDOW_SEC} seconds old: generate a fresh one, or use it to check that your endpoint does refuse a stale delivery.</p><pre tabindex="0"><code>${webhooks.curlFor(sample.headers, sample.body)}</code></pre></div>` : ''}`,
});
});
function verifyResult(v) {
function verifyResult(v) {
@@ -30,7 +30,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
@@ -30,7 +30,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
const flushList = () => { if (list) { out.push(`<${list.type}>${list.items.map((i) => `<li>${inline(i)}</li>`).join('')}</${list.type}>`); list = null; } };
const flushList = () => { if (list) { out.push(`<${list.type}>${list.items.map((i) => `<li>${inline(i)}</li>`).join('')}</${list.type}>`); list = null; } };
for (const line of lines) {
for (const line of lines) {
if (fence) {
if (fence) {
- if (/^```/.test(line)) { out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);
+ if (/^```/.test(line)) { out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`); fence = null; } else fence.push(line);
continue;
continue;
}
}
if (/^```/.test(line)) { flushPara(); flushList(); fence = []; continue; }
if (/^```/.test(line)) { flushPara(); flushList(); fence = []; continue; }
@@ -54,7 +54,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
@@ -54,7 +54,7 @@ function markdown(src, { headingOffset = 1 } = {}) {
flushList();
flushList();
para.push(line.trim());
para.push(line.trim());
}
}
- if (fence) out.push(`<pre><code>${esc(fence.join('\n'))}</code></pre>`);
+ if (fence) out.push(`<pre tabindex="0"><code>${esc(fence.join('\n'))}</code></pre>`);
flushPara(); flushList();
flushPara(); flushList();
return out.join('\n');
return out.join('\n');
}
}
@@ -120,6 +120,8 @@ const sdkPkg = require('openvibe-sdk/package.json');
@@ -120,6 +120,8 @@ const sdkPkg = require('openvibe-sdk/package.json');
assert.strictEqual(list.status, 200);
assert.strictEqual(list.status, 200);
for (const s of index) assert.ok(list.text.includes(`href="/docs/api/${s.service}"`), `${s.service} is listed`);
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');
assert.ok((await t.get('/docs')).text.includes('href="/docs/api"'), 'the docs index links it');
+ // Code blocks scroll sideways on a phone, so each is a keyboard stop (axe scrollable-region-focusable).
+ assert.ok(!/<pre(?![^>]*tabindex)[\s>]/.test((await t.get('/docs')).text), 'every <pre> on /docs is focusable');
const raw = await t.get('/docs/api/tools.json');
const raw = await t.get('/docs/api/tools.json');
assert.strictEqual(raw.status, 200);
assert.strictEqual(raw.status, 200);
assert.match(raw.headers.get('content-type'), /application\/vnd\.oai\.openapi\+json/);
assert.match(raw.headers.get('content-type'), /application\/vnd\.oai\.openapi\+json/);