From dfda06f1e611e84dcf9b2e45cd1a84efe42b43fe Mon Sep 17 00:00:00 2001 From: umair Date: Thu, 30 Jul 2026 14:09:56 +0100 Subject: [PATCH 1/3] ci: check that referenced ably.com doc links and their anchors still resolve Every `@see` and inline doc link on the public surface points at ably.com, which is restructured independently of this repository, so a link that was correct when merged can rot afterwards. The failure that matters is a stale fragment: a browser given an unknown fragment silently leaves the reader at the top of the page, so a dead anchor survives any check that only asserts a status code, and survives review. scripts/checkDocLinks.ts collects every ably.com/docs URL in ably.d.ts, modular.d.ts and src/, requests each page once following redirects, and asserts that the page returns 200 and that a URL carrying a fragment matches an element id on the page it resolves to. That last part has to inspect the served HTML rather than heading slugs, because the reference pages carry a member-name alias anchor inside each heading. The docs site rate-limits bursts, so pages are fetched serially with a pause between them and 429s and 5xx are retried with backoff. A full run takes about a minute. Thirteen URLs already fail on main, so they are recorded in KNOWN_BROKEN and the check gates new breakage instead of demanding the backlog be cleared first. The list cannot rot quietly: a listed URL that starts working is reported as a failure asking for its entry to be deleted, and entries carry the replacement target where one has been verified. Writing the check turned up a fourteenth, in a remediation string rather than a docstring: utils.ts points at /docs/channels#derived, and that page does not document derived channels at all. It is recorded rather than fixed here, since a remediation is not supposed to carry a URL in the first place, so the fix belongs with the error-string work. Runs on pull requests that touch the scanned files, on push to main, and weekly on a schedule to catch drift that happens with no commit of ours. Requested in review of #2243. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/docs-links.yml | 39 ++++ package.json | 1 + scripts/README.md | 14 ++ scripts/checkDocLinks.ts | 323 +++++++++++++++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 .github/workflows/docs-links.yml create mode 100644 scripts/checkDocLinks.ts diff --git a/.github/workflows/docs-links.yml b/.github/workflows/docs-links.yml new file mode 100644 index 0000000000..45d4cab6b9 --- /dev/null +++ b/.github/workflows/docs-links.yml @@ -0,0 +1,39 @@ +name: Doc Links +on: + pull_request: + paths: + - 'ably.d.ts' + - 'modular.d.ts' + - 'src/**' + - 'scripts/checkDocLinks.ts' + - '.github/workflows/docs-links.yml' + push: + branches: + - main + # The docs site is restructured independently of this repository, so a link that is + # correct when merged can rot later. The scheduled run catches that drift. + schedule: + - cron: '0 7 * * 1' + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 + with: + persist-credentials: false + + - name: Use Node.js 20.x + uses: actions/setup-node@3235b876344d2a9aa001b8d1453c930bba69e610 # v3 + with: + node-version: 20.x + + - name: Install Package Dependencies + run: npm ci + + - name: Check documentation links + run: npm run check-doc-links diff --git a/package.json b/package.json index 68a4bc2ba9..37a57712ea 100644 --- a/package.json +++ b/package.json @@ -205,6 +205,7 @@ "sourcemap": "source-map-explorer build/ably.min.js", "modulereport": "tsc --noEmit --esModuleInterop scripts/moduleReport.ts && esr scripts/moduleReport.ts", "speccoveragereport": "tsc --noEmit --esModuleInterop --target ES2017 --moduleResolution node scripts/specCoverageReport.ts && esr scripts/specCoverageReport.ts", + "check-doc-links": "tsc --noEmit --esModuleInterop --strictNullChecks --target ES2020 --moduleResolution node scripts/checkDocLinks.ts && esr scripts/checkDocLinks.ts", "process-private-api-data": "tsc --noEmit --esModuleInterop --strictNullChecks scripts/processPrivateApiData/run.ts && esr scripts/processPrivateApiData/run.ts", "docs": "typedoc" } diff --git a/scripts/README.md b/scripts/README.md index 526bfbe0bc..cf97299b96 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -26,3 +26,17 @@ source <(ably-env secrets print-aws) See [AWS Access](https://ably.atlassian.net/wiki/spaces/ENG/pages/665190401/AWS+Access) for more information about gaining access to AWS. + +### checkDocLinks.ts + +Verifies every `ably.com/docs` URL referenced from `ably.d.ts`, `modular.d.ts` and `src/`: +that the page still resolves, and that a URL carrying a fragment still matches an element +id on the page it resolves to. A stale fragment is the failure worth catching, since a +browser given an unknown fragment silently leaves the reader at the top of the page. + +Run with `npm run check-doc-links`. It takes about a minute: the docs site rate-limits +bursts, so pages are fetched one at a time. + +URLs that are already broken are listed in the script's `KNOWN_BROKEN` map so the check +gates new breakage rather than the existing backlog. Fixing one means deleting its entry — +a listed URL that starts working is reported as a failure telling you to remove it. diff --git a/scripts/checkDocLinks.ts b/scripts/checkDocLinks.ts new file mode 100644 index 0000000000..6e6f00512a --- /dev/null +++ b/scripts/checkDocLinks.ts @@ -0,0 +1,323 @@ +/** + * Verifies every ably.com documentation URL referenced from the library's public surface: + * that the page still resolves, and that a URL carrying a fragment still matches an element + * id on the page it resolves to. + * + * A fragment that no longer exists is the failure worth catching. A browser given an unknown + * fragment silently leaves the reader at the top of the page rather than reporting anything, + * so a stale `@see` anchor looks healthy in every check that only asserts a status code. + * + * Run with `npm run check-doc-links`. + */ +import fs from 'fs'; +import https from 'https'; +import { glob } from 'glob'; + +/** Declaration files scanned in full. */ +const DECLARATION_FILES = ['ably.d.ts', 'modular.d.ts']; + +/** Sources scanned for doc URLs in JSDoc comments and error strings. */ +const SOURCE_GLOB = 'src/**/*.ts'; + +/** Matches an ably.com docs URL, stopping at the punctuation that terminates one in prose or markdown. */ +const DOC_URL_PATTERN = /https:\/\/ably\.com\/docs\/[^\s)'"`<>\]]+/g; + +const REQUEST_TIMEOUT_MS = 30000; +const MAX_ATTEMPTS = 4; +const MAX_REDIRECTS = 5; + +/** + * ably.com rate-limits bursts of requests, so pages are fetched one at a time with a pause + * between them. The run costs well under a minute at the current URL count. + */ +const DELAY_BETWEEN_PAGES_MS = 1200; + +/** + * URLs that already fail, recorded so this check can gate new breakage without first + * requiring the backlog to be cleared. Each entry is debt to pay down, not a precedent to + * copy: every one is a legacy `/docs` path that now redirects to a restructured page where + * the original fragment does not exist, so the reader lands at the top of a page that may + * not even cover the referenced concept. + * + * Removing an entry is the goal. Nothing here rots silently: a listed URL that starts + * working is itself reported as a failure telling you to delete the line. + */ +const KNOWN_BROKEN: Record = { + 'https://ably.com/docs/core-features/authentication/#capabilities-explained': 'redirects to /docs/auth', + 'https://ably.com/docs/core-features/authentication#ably-jwt': 'redirects to /docs/auth', + 'https://ably.com/docs/core-features/authentication#ably-tokens': 'redirects to /docs/auth', + 'https://ably.com/docs/core-features/authentication#basic-authentication': 'redirects to /docs/auth', + 'https://ably.com/docs/core-features/authentication#identified-clients': + 'redirects to /docs/auth; use /docs/auth/identified-clients', + 'https://ably.com/docs/core-features/authentication#token-authentication': + 'redirects to /docs/auth; use /docs/auth/token', + 'https://ably.com/docs/realtime/channels#nonfatal-errors': 'redirects to /docs/channels', + 'https://ably.com/docs/realtime/channels#transient-publish': + 'redirects to /docs/channels; use /docs/pub-sub/advanced#transient-publish', + 'https://ably.com/docs/realtime/connection/#connection-state-recovery': 'redirects to /docs/connect', + 'https://ably.com/docs/realtime/encryption#getting-started': 'redirects to /docs/channels/options/encryption', + 'https://ably.com/docs/rest/channels#publish-on-behalf': + 'redirects to /docs/channels; use /docs/pub-sub/advanced#publish-on-behalf', + 'https://ably.com/docs/realtime/usage': 'page has been removed', + 'https://ably.com/docs/rest/usage': 'page has been removed', + // Found by this check: /docs/channels does not mention derived channels at all. The fix + // belongs with the error-string work rather than here, since a remediation is not supposed + // to carry a URL in the first place (see CLAUDE.md); the concept is now documented at + // /docs/pub-sub/advanced#subscription-filters. + 'https://ably.com/docs/channels#derived': 'page does not document derived channels', +}; + +interface Occurrence { + file: string; + line: number; +} + +interface PageResult { + status: number; + finalUrl: string; + /** Set only when the page could not be fetched at all, after every retry. */ + error?: string; + body?: string; +} + +type Outcome = 'ok' | 'broken' | 'known-broken' | 'fixed'; + +interface UrlReport { + url: string; + outcome: Outcome; + detail: string; + redirectedTo?: string; + occurrences: Occurrence[]; +} + +function escapeForRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** Splits a URL into the part to request and its fragment, keeping any query string on the page. */ +function splitFragment(url: string): { page: string; fragment: string | null } { + const hashIndex = url.indexOf('#'); + if (hashIndex === -1) { + return { page: url, fragment: null }; + } + return { page: url.slice(0, hashIndex), fragment: url.slice(hashIndex + 1) }; +} + +function collectUrls(files: string[]): Map { + const index = new Map(); + + for (const file of files) { + const lines = fs.readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, lineIndex) => { + for (const match of line.matchAll(DOC_URL_PATTERN)) { + // Trailing punctuation belongs to the sentence, not the URL. + const url = match[0].replace(/[.,;:]+$/, ''); + const occurrences = index.get(url) ?? []; + occurrences.push({ file, line: lineIndex + 1 }); + index.set(url, occurrences); + } + }); + } + + return index; +} + +function get( + url: string, +): Promise<{ status: number; location: string | null; body: string; retryAfter: number | null }> { + return new Promise((resolve, reject) => { + const request = https.get(url, { headers: { 'user-agent': 'ably-js-doc-link-check' } }, (response) => { + const status = response.statusCode ?? 0; + const location = (response.headers.location as string | undefined) ?? null; + const retryAfterHeader = response.headers['retry-after']; + const retryAfter = typeof retryAfterHeader === 'string' ? Number(retryAfterHeader) : null; + + // A body is only needed when the anchors on the final page get inspected. + if (status >= 300) { + response.resume(); + resolve({ status, location, body: '', retryAfter: Number.isFinite(retryAfter) ? retryAfter : null }); + return; + } + + const chunks: Buffer[] = []; + response.on('data', (chunk: Buffer) => chunks.push(chunk)); + response.on('end', () => + resolve({ status, location, body: Buffer.concat(chunks).toString('utf8'), retryAfter: null }), + ); + response.on('error', reject); + }); + + request.setTimeout(REQUEST_TIMEOUT_MS, () => request.destroy(new Error(`timed out after ${REQUEST_TIMEOUT_MS}ms`))); + request.on('error', reject); + }); +} + +function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** Fetches a page, following redirects, retrying the failures that are worth retrying. */ +async function fetchPage(pageUrl: string): Promise { + let lastError = ''; + + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + let currentUrl = pageUrl; + + for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect++) { + if (redirect === MAX_REDIRECTS) { + return { status: 0, finalUrl: currentUrl, error: `more than ${MAX_REDIRECTS} redirects` }; + } + + const response = await get(currentUrl); + + if (response.status >= 300 && response.status < 400 && response.location) { + currentUrl = new URL(response.location, currentUrl).toString(); + continue; + } + + // Rate limiting and server faults are transient; anything else is the answer. + if (response.status === 429 || response.status >= 500) { + const backoffMs = (response.retryAfter ?? 2 ** attempt) * 1000; + lastError = `HTTP ${response.status}`; + await delay(backoffMs); + break; + } + + return { status: response.status, finalUrl: currentUrl, body: response.body }; + } + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + await delay(2 ** attempt * 1000); + } + } + + return { status: 0, finalUrl: pageUrl, error: lastError }; +} + +function hasAnchor(body: string, fragment: string): boolean { + const escaped = escapeForRegExp(decodeURIComponent(fragment)); + // The reference pages carry a member-name alias anchor inside the heading element + // (``), so the id being matched is not necessarily the heading's own slug. + return new RegExp(`(?:id|name)=["']${escaped}["']`).test(body); +} + +async function main(): Promise { + const files = [...DECLARATION_FILES, ...(await glob(SOURCE_GLOB))].filter((file) => fs.existsSync(file)); + const urls = collectUrls(files); + + if (urls.size === 0) { + console.error(`No ably.com doc URLs found in ${files.length} files. The URL pattern is probably wrong.`); + process.exit(1); + } + + // One request per page, however many fragments point at it. + const pages = new Map(); + for (const url of urls.keys()) { + const { page } = splitFragment(url); + pages.set(page, [...(pages.get(page) ?? []), url]); + } + + console.log(`Checking ${urls.size} doc URLs across ${pages.size} pages, from ${files.length} files.\n`); + + const reports: UrlReport[] = []; + let pagesFetched = 0; + + for (const [page, pageUrls] of pages) { + if (pagesFetched > 0) { + await delay(DELAY_BETWEEN_PAGES_MS); + } + const result = await fetchPage(page); + pagesFetched++; + + for (const url of pageUrls) { + const { fragment } = splitFragment(url); + const occurrences = urls.get(url) ?? []; + const knownReason = KNOWN_BROKEN[url]; + const redirectedTo = result.finalUrl === page ? undefined : result.finalUrl; + + let detail = ''; + if (result.status === 0) { + detail = `request failed: ${result.error}`; + } else if (result.status !== 200) { + detail = `HTTP ${result.status}`; + } else if (fragment && !hasAnchor(result.body ?? '', fragment)) { + detail = `page has no element with id "${fragment}"`; + } + + if (detail === '') { + reports.push({ + url, + outcome: knownReason ? 'fixed' : 'ok', + detail: knownReason ? `listed as known-broken (${knownReason}) but now resolves` : '', + redirectedTo, + occurrences, + }); + } else { + reports.push({ + url, + outcome: knownReason ? 'known-broken' : 'broken', + detail, + redirectedTo, + occurrences, + }); + } + } + } + + const describe = (report: UrlReport) => { + const where = report.occurrences.map((occurrence) => `${occurrence.file}:${occurrence.line}`).join(', '); + const redirect = report.redirectedTo ? `\n redirects to ${report.redirectedTo}` : ''; + return ` ${report.url}\n ${report.detail}${redirect}\n referenced at ${where}`; + }; + + const broken = reports.filter((report) => report.outcome === 'broken'); + const fixed = reports.filter((report) => report.outcome === 'fixed'); + const knownBroken = reports.filter((report) => report.outcome === 'known-broken'); + const redirected = reports.filter((report) => report.outcome === 'ok' && report.redirectedTo); + + if (redirected.length > 0) { + console.log(`Resolving via a redirect (${redirected.length}) — worth updating, not failing the check:`); + for (const report of redirected) { + console.log(` ${report.url}\n redirects to ${report.redirectedTo}`); + } + console.log(''); + } + + if (knownBroken.length > 0) { + console.log(`Known broken (${knownBroken.length}), listed in KNOWN_BROKEN:`); + for (const report of knownBroken) { + console.log(` ${report.url} — ${report.detail}`); + } + console.log(''); + } + + if (fixed.length > 0) { + console.log(`Fixed (${fixed.length}) — remove these from KNOWN_BROKEN in scripts/checkDocLinks.ts:`); + for (const report of fixed) { + console.log(describe(report)); + } + console.log(''); + } + + if (broken.length > 0) { + console.log(`Broken (${broken.length}):`); + for (const report of broken) { + console.log(describe(report)); + } + console.log(''); + } + + const checked = reports.length; + const healthy = reports.filter((report) => report.outcome === 'ok').length; + console.log(`${healthy}/${checked} URLs resolve, ${knownBroken.length} known broken.`); + + if (broken.length > 0 || fixed.length > 0) { + process.exit(1); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); From 038e8899e98b97a089c64bfa967b5e5bf9a8bbfb Mon Sep 17 00:00:00 2001 From: umair Date: Thu, 30 Jul 2026 14:12:42 +0100 Subject: [PATCH 2/3] ci: check out submodules for the doc-link job `npm ci` runs the `prepare` script, and the build type-checks test sources that import from the test/common/ably-common submodule, so the install aborted before the link check ran. Every other workflow in the repository checks out submodules recursively. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/docs-links.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs-links.yml b/.github/workflows/docs-links.yml index 45d4cab6b9..3b5d61b319 100644 --- a/.github/workflows/docs-links.yml +++ b/.github/workflows/docs-links.yml @@ -23,8 +23,11 @@ jobs: check: runs-on: ubuntu-latest steps: + # Submodules are needed because `npm ci` runs the `prepare` script, and the build + # type-checks test sources that import from test/common/ably-common. - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 with: + submodules: 'recursive' persist-credentials: false - name: Use Node.js 20.x From 9c030f905587362e2feed7002710ff58b5be3098 Mon Sep 17 00:00:00 2001 From: umair Date: Thu, 30 Jul 2026 14:30:04 +0100 Subject: [PATCH 3/3] docs: repoint the 14 doc links the new check reported as broken Each replacement was chosen by reading the current ably/docs sources to find where the concept moved, then verified live: the page returns 200 and, where the URL carries a fragment, the page has an element with that id. - the /docs/core-features/authentication family, which redirected to /docs/auth where none of its fragments survived, now points at the pages that replaced those sections: /docs/auth/capabilities, /docs/auth/basic, /docs/auth/identified-clients, /docs/auth/token, /docs/auth/token/ably-tokens and /docs/auth/token/jwt. That also fixes a "authentication.ably-jwt" typo whose broken fragment was hidden by the redirect - transient publishing and publishing on behalf of a connection moved to /docs/pub-sub/advanced; connection state recovery to /docs/connect/states; channel encryption to /docs/channels/options/encryption#encrypt - ChannelStateChange.resumed now points at /docs/channels/states#non-fatal-errors. The channel page is the right one, not the similarly named connection-level section on /docs/connect/states, and its fragment is hyphenated differently Two needed more than a new URL: - the module header linked /docs/realtime/usage and /docs/rest/usage, both now 404 with no one-to-one successor: /docs/basics carries `redirect_from: /docs/realtime, /docs/rest`, so the docs no longer split the two interfaces. It now points at the JavaScript quickstart and /docs/basics, which also retires a redirect hop through /docs/quick-start-guide - utils.ts pointed a remediation at /docs/channels#derived, a page that does not document derived channels. The URL is dropped rather than repointed, because a remediation is not supposed to carry a link at all (see CLAUDE.md) and the sentence is already actionable without one KNOWN_BROKEN is empty as a result. The mechanism stays as an escape hatch for a link that cannot be fixed at once, since a listed URL that starts working is still reported as a failure asking for its entry to be removed. The check now reports 29/29 URLs resolving. Seven URLs that resolve only via a redirect are left alone: they are reported as worth updating rather than failing the run. Co-Authored-By: Claude Opus 5 (1M context) --- ably.d.ts | 34 +++++++++++++++++----------------- modular.d.ts | 2 +- scripts/README.md | 7 ++++--- scripts/checkDocLinks.ts | 35 +++++------------------------------ src/common/lib/util/utils.ts | 2 +- 5 files changed, 28 insertions(+), 52 deletions(-) diff --git a/ably.d.ts b/ably.d.ts index b22843ffa9..b428d6f136 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -6,7 +6,7 @@ /** * You are currently viewing the default variant of the Ably JavaScript Client Library SDK. View the modular variant {@link modular | here}. * - * To get started with the Ably JavaScript Client Library SDK, follow the [Quickstart Guide](https://ably.com/docs/quick-start-guide) or view the introductions to the [realtime](https://ably.com/docs/realtime/usage) and [REST](https://ably.com/docs/rest/usage) interfaces. + * To get started with the Ably JavaScript Client Library SDK, follow the [JavaScript quickstart](https://ably.com/docs/getting-started/javascript) or read [about Ably Pub/Sub](https://ably.com/docs/basics). * * @module */ @@ -478,7 +478,7 @@ export interface ClientOptions extends AuthOptions { }; /** - * Enables a connection to inherit the state of a previous connection that may have existed under a different instance of the Realtime library. This might typically be used by clients of the browser library to ensure connection state can be preserved when the user refreshes the page. A recovery key string can be explicitly provided, or alternatively if a callback function is provided, the client library will automatically persist the recovery key between page reloads and call the callback when the connection is recoverable. The callback is then responsible for confirming whether the connection should be recovered or not. See [connection state recovery](https://ably.com/docs/realtime/connection/#connection-state-recovery) for further information. + * Enables a connection to inherit the state of a previous connection that may have existed under a different instance of the Realtime library. This might typically be used by clients of the browser library to ensure connection state can be preserved when the user refreshes the page. A recovery key string can be explicitly provided, or alternatively if a callback function is provided, the client library will automatically persist the recovery key between page reloads and call the callback when the connection is recoverable. The callback is then responsible for confirming whether the connection should be recovered or not. See [connection state recovery](https://ably.com/docs/connect/states#connection-state-recovery) for further information. */ recover?: string | recoverConnectionCallback; @@ -653,15 +653,15 @@ export interface CorePlugins { */ export interface AuthOptions { /** - * Called when a new token is required. The role of the callback is to obtain a fresh token, one of: an Ably Token string (in plain text format); a signed {@link TokenRequest}; a {@link TokenDetails} (in JSON format); an [Ably JWT](https://ably.com/docs/core-features/authentication.ably-jwt). See [the authentication documentation](https://ably.com/docs/realtime/authentication) for details of the Ably {@link TokenRequest} format and associated API calls. + * Called when a new token is required. The role of the callback is to obtain a fresh token, one of: an Ably Token string (in plain text format); a signed {@link TokenRequest}; a {@link TokenDetails} (in JSON format); an [Ably JWT](https://ably.com/docs/auth/token/jwt). See [the authentication documentation](https://ably.com/docs/realtime/authentication) for details of the Ably {@link TokenRequest} format and associated API calls. * * @param data - The parameters that should be used to generate the token. - * @param callback - A function which, upon success, the `authCallback` should call with one of: an Ably Token string (in plain text format); a signed `TokenRequest`; a `TokenDetails` (in JSON format); an [Ably JWT](https://ably.com/docs/core-features/authentication#ably-jwt). Upon failure, the `authCallback` should call this function with information about the error. + * @param callback - A function which, upon success, the `authCallback` should call with one of: an Ably Token string (in plain text format); a signed `TokenRequest`; a `TokenDetails` (in JSON format); an [Ably JWT](https://ably.com/docs/auth/token/jwt). Upon failure, the `authCallback` should call this function with information about the error. */ authCallback?( data: TokenParams, /** - * A function which, upon success, the `authCallback` should call with one of: an Ably Token string (in plain text format); a signed `TokenRequest`; a `TokenDetails` (in JSON format); an [Ably JWT](https://ably.com/docs/core-features/authentication#ably-jwt). Upon failure, the `authCallback` should call this function with information about the error. + * A function which, upon success, the `authCallback` should call with one of: an Ably Token string (in plain text format); a signed `TokenRequest`; a `TokenDetails` (in JSON format); an [Ably JWT](https://ably.com/docs/auth/token/jwt). Upon failure, the `authCallback` should call this function with information about the error. * * @param error - Should be `null` if the auth request completed successfully, or containing details of the error if not. * @param tokenRequestOrDetails - A valid `TokenRequest`, `TokenDetails` or Ably JWT to be used for authentication. @@ -695,7 +695,7 @@ export interface AuthOptions { authUrl?: string; /** - * The full API key string, as obtained from the [Ably dashboard](https://ably.com/dashboard). Use this option if you wish to use Basic authentication, or wish to be able to issue Ably Tokens without needing to defer to a separate entity to sign Ably {@link TokenRequest | `TokenRequest`s}. Read more about [Basic authentication](https://ably.com/docs/core-features/authentication#basic-authentication). + * The full API key string, as obtained from the [Ably dashboard](https://ably.com/dashboard). Use this option if you wish to use Basic authentication, or wish to be able to issue Ably Tokens without needing to defer to a separate entity to sign Ably {@link TokenRequest | `TokenRequest`s}. Read more about [Basic authentication](https://ably.com/docs/auth/basic). */ key?: string; @@ -707,17 +707,17 @@ export interface AuthOptions { queryTime?: boolean; /** - * An authenticated token. This can either be a {@link TokenDetails} object or token string (obtained from the `token` property of a {@link TokenDetails} component of an Ably {@link TokenRequest} response, or a JSON Web Token satisfying [the Ably requirements for JWTs](https://ably.com/docs/core-features/authentication#ably-jwt)). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Read more about [Token authentication](https://ably.com/docs/core-features/authentication#token-authentication). + * An authenticated token. This can either be a {@link TokenDetails} object or token string (obtained from the `token` property of a {@link TokenDetails} component of an Ably {@link TokenRequest} response, or a JSON Web Token satisfying [the Ably requirements for JWTs](https://ably.com/docs/auth/token/jwt)). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Read more about [Token authentication](https://ably.com/docs/auth/token). */ token?: TokenDetails | string; /** - * An authenticated {@link TokenDetails} object (most commonly obtained from an Ably Token Request response). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Use this option if you wish to use Token authentication. Read more about [Token authentication](https://ably.com/docs/core-features/authentication#token-authentication). + * An authenticated {@link TokenDetails} object (most commonly obtained from an Ably Token Request response). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Use this option if you wish to use Token authentication. Read more about [Token authentication](https://ably.com/docs/auth/token). */ tokenDetails?: TokenDetails; /** - * When `true`, forces token authentication to be used by the library. If a `clientId` is not specified in the {@link ClientOptions} or {@link TokenParams}, then the Ably Token issued is [anonymous](https://ably.com/docs/core-features/authentication#identified-clients). + * When `true`, forces token authentication to be used by the library. If a `clientId` is not specified in the {@link ClientOptions} or {@link TokenParams}, then the Ably Token issued is [anonymous](https://ably.com/docs/auth/identified-clients). */ useTokenAuth?: boolean; @@ -759,13 +759,13 @@ export type CapabilityOp = capabilityOp; */ export interface TokenParams { /** - * The capabilities associated with this Ably Token. The capabilities value is a JSON-encoded representation of the resource paths and associated operations. Read more about capabilities in the [capabilities docs](https://ably.com/docs/core-features/authentication/#capabilities-explained). + * The capabilities associated with this Ably Token. The capabilities value is a JSON-encoded representation of the resource paths and associated operations. Read more about capabilities in the [capabilities docs](https://ably.com/docs/auth/capabilities). * * @defaultValue `'{"*":["*"]}'` */ capability?: { [key: string]: capabilityOp[] | ['*'] } | string; /** - * A client ID, used for identifying this client when publishing messages or for presence purposes. The `clientId` can be any non-empty string, except it cannot contain a `*`. This option is primarily intended to be used in situations where the library is instantiated with a key. Note that a `clientId` may also be implicit in a token used to instantiate the library. An error is raised if a `clientId` specified here conflicts with the `clientId` implicit in the token. Find out more about [identified clients](https://ably.com/docs/core-features/authentication#identified-clients). + * A client ID, used for identifying this client when publishing messages or for presence purposes. The `clientId` can be any non-empty string, except it cannot contain a `*`. This option is primarily intended to be used in situations where the library is instantiated with a key. Note that a `clientId` may also be implicit in a token used to instantiate the library. An error is raised if a `clientId` specified here conflicts with the `clientId` implicit in the token. Find out more about [identified clients](https://ably.com/docs/auth/identified-clients). */ clientId?: string; /** @@ -815,11 +815,11 @@ export interface CipherParams { */ export interface TokenDetails { /** - * The capabilities associated with this Ably Token. The capabilities value is a JSON-encoded representation of the resource paths and associated operations. Read more about capabilities in the [capabilities docs](https://ably.com/docs/core-features/authentication/#capabilities-explained). + * The capabilities associated with this Ably Token. The capabilities value is a JSON-encoded representation of the resource paths and associated operations. Read more about capabilities in the [capabilities docs](https://ably.com/docs/auth/capabilities). */ capability: string; /** - * The client ID, if any, bound to this Ably Token. If a client ID is included, then the Ably Token authenticates its bearer as that client ID, and the Ably Token may only be used to perform operations on behalf of that client ID. The client is then considered to be an [identified client](https://ably.com/docs/core-features/authentication#identified-clients). + * The client ID, if any, bound to this Ably Token. If a client ID is included, then the Ably Token authenticates its bearer as that client ID, and the Ably Token may only be used to perform operations on behalf of that client ID. The client is then considered to be an [identified client](https://ably.com/docs/auth/identified-clients). */ clientId?: string; /** @@ -831,7 +831,7 @@ export interface TokenDetails { */ issued: number; /** - * The [Ably Token](https://ably.com/docs/core-features/authentication#ably-tokens) itself. A typical Ably Token string appears with the form `xVLyHw.A-pwh7wicf3afTfgiw4k2Ku33kcnSA7z6y8FjuYpe3QaNRTEo4`. + * The [Ably Token](https://ably.com/docs/auth/token/ably-tokens) itself. A typical Ably Token string appears with the form `xVLyHw.A-pwh7wicf3afTfgiw4k2Ku33kcnSA7z6y8FjuYpe3QaNRTEo4`. */ token: string; } @@ -990,7 +990,7 @@ export type ResolvedChannelMode = */ export interface ChannelOptions { /** - * Requests encryption for this channel when not null, and specifies encryption-related parameters (such as algorithm, chaining mode, key length and key). See [an example](https://ably.com/docs/realtime/encryption#getting-started). When running in a browser, encryption is only available when the current environment is a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). + * Requests encryption for this channel when not null, and specifies encryption-related parameters (such as algorithm, chaining mode, key length and key). See [an example](https://ably.com/docs/channels/options/encryption#encrypt). When running in a browser, encryption is only available when the current environment is a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). */ cipher?: CipherParamOptions | CipherParams; /** @@ -1155,7 +1155,7 @@ export interface ChannelStateChange { */ reason?: ErrorInfo; /** - * Indicates whether message continuity on this channel is preserved, see [Nonfatal channel errors](https://ably.com/docs/realtime/channels#nonfatal-errors) for more info. + * Indicates whether message continuity on this channel is preserved, see [Nonfatal channel errors](https://ably.com/docs/channels/states#non-fatal-errors) for more info. */ resumed: boolean; /** @@ -3744,7 +3744,7 @@ export declare interface Connection */ id?: string; /** - * A unique private connection key used to recover or resume a connection, assigned by Ably. This private connection key can also be used by other REST clients to publish on behalf of this client. See the [publishing over REST on behalf of a realtime client docs](https://ably.com/docs/rest/channels#publish-on-behalf) for more info. (If you want to explicitly recover a connection in a different SDK instance, see createRecoveryKey() instead) + * A unique private connection key used to recover or resume a connection, assigned by Ably. This private connection key can also be used by other REST clients to publish on behalf of this client. See the [publishing over REST on behalf of a realtime client docs](https://ably.com/docs/pub-sub/advanced#publish-on-behalf) for more info. (If you want to explicitly recover a connection in a different SDK instance, see createRecoveryKey() instead) */ key?: string; /** diff --git a/modular.d.ts b/modular.d.ts index bed13ff4e1..133de68041 100644 --- a/modular.d.ts +++ b/modular.d.ts @@ -1,7 +1,7 @@ /** * You are currently viewing the modular (tree-shakable) variant of the Ably JavaScript Client Library SDK. View the default variant {@link ably | here}. * - * To get started with the Ably JavaScript Client Library SDK, follow the [Quickstart Guide](https://ably.com/docs/quick-start-guide) or view the introductions to the [realtime](https://ably.com/docs/realtime/usage) and [REST](https://ably.com/docs/rest/usage) interfaces. + * To get started with the Ably JavaScript Client Library SDK, follow the [JavaScript quickstart](https://ably.com/docs/getting-started/javascript) or read [about Ably Pub/Sub](https://ably.com/docs/basics). * * ## No `static` class functionality * diff --git a/scripts/README.md b/scripts/README.md index cf97299b96..32c977bd88 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -37,6 +37,7 @@ browser given an unknown fragment silently leaves the reader at the top of the p Run with `npm run check-doc-links`. It takes about a minute: the docs site rate-limits bursts, so pages are fetched one at a time. -URLs that are already broken are listed in the script's `KNOWN_BROKEN` map so the check -gates new breakage rather than the existing backlog. Fixing one means deleting its entry — -a listed URL that starts working is reported as a failure telling you to remove it. +A URL that is known to be broken and cannot be fixed immediately can be listed in the +script's `KNOWN_BROKEN` map, so that the check gates new breakage rather than blocking on a +backlog. Fixing one means deleting its entry — a listed URL that starts working is reported +as a failure telling you to remove it. The map is currently empty. diff --git a/scripts/checkDocLinks.ts b/scripts/checkDocLinks.ts index 6e6f00512a..b965288213 100644 --- a/scripts/checkDocLinks.ts +++ b/scripts/checkDocLinks.ts @@ -33,39 +33,14 @@ const MAX_REDIRECTS = 5; const DELAY_BETWEEN_PAGES_MS = 1200; /** - * URLs that already fail, recorded so this check can gate new breakage without first - * requiring the backlog to be cleared. Each entry is debt to pay down, not a precedent to - * copy: every one is a legacy `/docs` path that now redirects to a restructured page where - * the original fragment does not exist, so the reader lands at the top of a page that may - * not even cover the referenced concept. + * An escape hatch for a URL that is known to be broken and cannot be fixed immediately, so + * that the check gates new breakage rather than blocking on a backlog. Map the URL to the + * reason it is listed. * - * Removing an entry is the goal. Nothing here rots silently: a listed URL that starts + * Prefer fixing the link. Nothing listed here rots silently: a listed URL that starts * working is itself reported as a failure telling you to delete the line. */ -const KNOWN_BROKEN: Record = { - 'https://ably.com/docs/core-features/authentication/#capabilities-explained': 'redirects to /docs/auth', - 'https://ably.com/docs/core-features/authentication#ably-jwt': 'redirects to /docs/auth', - 'https://ably.com/docs/core-features/authentication#ably-tokens': 'redirects to /docs/auth', - 'https://ably.com/docs/core-features/authentication#basic-authentication': 'redirects to /docs/auth', - 'https://ably.com/docs/core-features/authentication#identified-clients': - 'redirects to /docs/auth; use /docs/auth/identified-clients', - 'https://ably.com/docs/core-features/authentication#token-authentication': - 'redirects to /docs/auth; use /docs/auth/token', - 'https://ably.com/docs/realtime/channels#nonfatal-errors': 'redirects to /docs/channels', - 'https://ably.com/docs/realtime/channels#transient-publish': - 'redirects to /docs/channels; use /docs/pub-sub/advanced#transient-publish', - 'https://ably.com/docs/realtime/connection/#connection-state-recovery': 'redirects to /docs/connect', - 'https://ably.com/docs/realtime/encryption#getting-started': 'redirects to /docs/channels/options/encryption', - 'https://ably.com/docs/rest/channels#publish-on-behalf': - 'redirects to /docs/channels; use /docs/pub-sub/advanced#publish-on-behalf', - 'https://ably.com/docs/realtime/usage': 'page has been removed', - 'https://ably.com/docs/rest/usage': 'page has been removed', - // Found by this check: /docs/channels does not mention derived channels at all. The fix - // belongs with the error-string work rather than here, since a remediation is not supposed - // to carry a URL in the first place (see CLAUDE.md); the concept is now documented at - // /docs/pub-sub/advanced#subscription-filters. - 'https://ably.com/docs/channels#derived': 'page does not document derived channels', -}; +const KNOWN_BROKEN: Record = {}; interface Occurrence { file: string; diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index 5505422873..dd42898a04 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -479,7 +479,7 @@ export function matchDerivedChannel(name: string) { statusCode: 400, remediation: 'Pass a non-empty channel name to channels.getDerived(name, { filter: ... }) and put the filter expression in the filter option, not in the name. ' + - 'A channel-params prefix such as "[?rewind=1]foo" is allowed. See https://ably.com/docs/channels#derived.', + 'A channel-params prefix such as "[?rewind=1]foo" is allowed.', }); } // Fail if there is already a channel qualifier, eg [meta]foo should fail instead of just overriding with [filter=xyz]foo