-
-
Notifications
You must be signed in to change notification settings - Fork 10
feat(docs): embed markdown guides in Scalar API reference #3453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| /** | ||
| * Markdown guide loader for the Scalar API reference. | ||
| * | ||
| * Per-module markdown guides live under `modules/{name}/doc/guides/*.md` | ||
| * and are discovered by the same globbing mechanism as OpenAPI YAML files | ||
| * (see `config/assets.js` → `allGuides`). | ||
| * | ||
| * Guides are merged into the OpenAPI spec via `info.description`, which | ||
| * Scalar renders as a top-level "Introduction" section in the sidebar and | ||
| * splits on markdown H1/H2 headings. | ||
| */ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| import logger from '../services/logger.js'; | ||
|
|
||
| /** | ||
| * Derive a human-readable title from a guide file path. | ||
| * E.g. `modules/auth/doc/guides/getting-started.md` → `Getting Started` | ||
| * @param {string} filePath - Absolute or relative path to the guide file. | ||
| * @returns {string} Title-cased guide name. | ||
| */ | ||
| const titleFromPath = (filePath) => { | ||
| const base = path.basename(String(filePath), path.extname(String(filePath))); | ||
| return base | ||
| .split(/[-_\s]+/) | ||
| .filter(Boolean) | ||
| .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||
| .join(' '); | ||
| }; | ||
|
|
||
| /** | ||
| * Strip the first H1 heading from a markdown body (if present). | ||
| * The loader injects its own H1 based on the file name so Scalar's sidebar | ||
| * stays consistent even when guides omit a title or use a different one. | ||
| * @param {string} markdown - Raw markdown content. | ||
| * @returns {string} Markdown without the leading H1. | ||
| */ | ||
| const stripLeadingH1 = (markdown) => String(markdown).replace(/^\s*#\s+[^\n]*\n+/, ''); | ||
|
|
||
| /** | ||
| * Load markdown guides from disk and return normalized entries. | ||
| * Invalid/unreadable files are skipped with a warning so one broken guide | ||
| * cannot take down the whole API reference. | ||
| * @param {string[]} filePaths - Absolute paths to `.md` guide files. | ||
| * @returns {{ title: string, body: string, path: string }[]} Loaded guides. | ||
| */ | ||
| const loadGuides = (filePaths) => { | ||
| if (!Array.isArray(filePaths) || filePaths.length === 0) return []; | ||
| return filePaths | ||
| .map((filePath) => { | ||
| try { | ||
| const raw = fs.readFileSync(filePath, 'utf8'); | ||
| const body = stripLeadingH1(raw).trim(); | ||
| if (!body) { | ||
| logger.warn(`[guides] skipping ${filePath}: empty markdown content`); | ||
| return null; | ||
| } | ||
| return { title: titleFromPath(filePath), body, path: filePath }; | ||
| } catch (err) { | ||
| logger.warn(`[guides] failed to load ${filePath}: ${err.message}`); | ||
| return null; | ||
| } | ||
| }) | ||
| .filter(Boolean) | ||
| // Stable alphabetical order so the sidebar is deterministic across | ||
| // filesystems (glob order varies on macOS vs Linux containers). | ||
| .sort((a, b) => a.title.localeCompare(b.title)); | ||
| }; | ||
|
|
||
| /** | ||
| * Merge loaded guides into an OpenAPI spec's `info.description`. | ||
| * Each guide becomes a top-level H1 section, which Scalar renders as a | ||
| * sidebar entry alongside the API reference. | ||
| * | ||
| * The original spec is mutated (and returned) to match the merge style used | ||
| * by `initSwagger` in `lib/services/express.js`. | ||
| * | ||
| * @param {object} spec - OpenAPI spec object (will be mutated). | ||
| * @param {{ title: string, body: string }[]} guides - Loaded guide entries. | ||
| * @returns {object} The same spec, with guides appended to `info.description`. | ||
| */ | ||
| const mergeGuidesIntoSpec = (spec, guides) => { | ||
| if (!spec || typeof spec !== 'object') return spec; | ||
| if (!Array.isArray(guides) || guides.length === 0) return spec; | ||
|
|
||
| const sections = guides.map(({ title, body }) => `# ${title}\n\n${body}`); | ||
| const existing = typeof spec.info?.description === 'string' ? spec.info.description.trim() : ''; | ||
| const merged = [existing, ...sections].filter(Boolean).join('\n\n'); | ||
|
|
||
| spec.info = { ...(spec.info || {}), description: merged }; | ||
| return spec; | ||
| }; | ||
|
|
||
| export default { | ||
| titleFromPath, | ||
| stripLeadingH1, | ||
| loadGuides, | ||
| mergeGuidesIntoSpec, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # Authentication | ||
|
|
||
| The API uses JWT authentication delivered via an `httpOnly` `TOKEN` | ||
| cookie. Clients do not receive the token in the response body — they | ||
| receive user metadata and a `tokenExpiresIn` timestamp, and subsequent | ||
| requests are authenticated automatically as long as the cookie is sent. | ||
|
|
||
| ## Sign up | ||
|
|
||
| Create a new account by sending a POST request: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/api/auth/signup \ | ||
| -H "Content-Type: application/json" \ | ||
| -c cookies.txt \ | ||
| -d '{ "email": "user@example.com", "password": "YourPassword1!" }' | ||
| ``` | ||
|
|
||
| If email verification is enabled, you will receive a confirmation link. | ||
| On success the response sets the `TOKEN` cookie and returns a body like: | ||
|
|
||
| ```json | ||
| { "user": { "id": "...", "email": "user@example.com" }, "tokenExpiresIn": 1735689600000 } | ||
| ``` | ||
|
|
||
| ## Log in | ||
|
|
||
| Authenticate with your credentials: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/api/auth/signin \ | ||
| -H "Content-Type: application/json" \ | ||
| -c cookies.txt \ | ||
| -d '{ "email": "user@example.com", "password": "YourPassword1!" }' | ||
| ``` | ||
|
|
||
| The response sets the `TOKEN` cookie and returns the user, their CASL | ||
| abilities, and `tokenExpiresIn` (epoch ms at which the JWT expires). | ||
|
|
||
| ## Using the token | ||
|
|
||
| Send the cookie on every protected request — the JWT is extracted from | ||
| the `TOKEN` cookie by the passport strategy: | ||
|
|
||
| ```bash | ||
| curl http://localhost:3000/api/users/me \ | ||
| -b cookies.txt | ||
| ``` | ||
|
|
||
| Browser clients get this for free: the cookie is `httpOnly`, `Secure`, | ||
| and `SameSite`-configured, so it is attached automatically to same-site | ||
| requests. | ||
|
|
||
| ## Token lifetime | ||
|
|
||
| The JWT lifetime is controlled server-side by `config.jwt.expiresIn`. | ||
| The signin/signup responses expose `tokenExpiresIn` so clients can | ||
| proactively re-authenticate before expiry. There is no refresh-token | ||
| endpoint — call `/api/auth/signin` again when the token expires. | ||
|
|
||
| ## Password reset | ||
|
|
||
| Request a reset email, then confirm with the token received: | ||
|
|
||
| ```bash | ||
| # Request reset | ||
| curl -X POST http://localhost:3000/api/auth/forgot \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ "email": "user@example.com" }' | ||
|
|
||
| # Confirm reset | ||
| curl -X POST http://localhost:3000/api/auth/reset \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ "token": "<resetToken>", "newPassword": "NewPassword1!" }' | ||
| ``` | ||
|
|
||
| ## Next steps | ||
|
|
||
| - See the **Organizations** guide to create teams and manage roles. | ||
| - Browse the endpoint reference for the full list of auth routes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Getting Started | ||
|
|
||
| Welcome to the Devkit Node API. This guide walks you through running the | ||
| backend locally and making your first API call. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - **Node.js** 22+ and npm | ||
| - **MongoDB** running locally or accessible via a connection string | ||
| - **Git** for cloning the repository | ||
|
|
||
| ## Setup | ||
|
|
||
| 1. Clone the Node backend repository. | ||
| 2. Copy `.env.example` to `.env` and fill in your values (mongo URI, JWT | ||
| secret, mail provider, etc.). | ||
| 3. Install dependencies: | ||
|
|
||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| 4. Start the development server: | ||
|
|
||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| The API listens on `http://localhost:3000` by default. | ||
|
|
||
| ## Your first API call | ||
|
|
||
| Once the server is running, verify it responds: | ||
|
|
||
| ```bash | ||
| curl http://localhost:3000/api/core/status | ||
| ``` | ||
|
|
||
| You should receive a JSON response confirming the server is healthy. | ||
|
|
||
| ## Explore the API | ||
|
|
||
| - **Authentication** — sign up, log in, and manage tokens | ||
| - **Organizations** — create teams and manage roles | ||
| - Browse the endpoint reference in the sidebar for full request/response | ||
| schemas and interactive examples |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| # Organizations | ||
|
|
||
| Organizations let you group users under a shared context with role-based | ||
| access control. | ||
|
|
||
| All examples below assume you are already authenticated and send the | ||
| `TOKEN` cookie set at signin (see the **Authentication** guide). | ||
|
|
||
| ## Creating an organization | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/api/organizations \ | ||
| -H "Content-Type: application/json" \ | ||
| -b cookies.txt \ | ||
| -d '{ "name": "My Team" }' | ||
| ``` | ||
PierreBrisorgueil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The creator is automatically assigned the **owner** role. | ||
|
|
||
| ## Listing organizations | ||
|
|
||
| Retrieve all organizations you belong to: | ||
|
|
||
| ```bash | ||
| curl http://localhost:3000/api/organizations \ | ||
| -b cookies.txt | ||
| ``` | ||
|
|
||
| ## Inviting members | ||
|
|
||
| Invite a user by email. They receive an invitation they can accept or | ||
| decline: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/api/organizations/<orgId>/invites \ | ||
| -H "Content-Type: application/json" \ | ||
| -b cookies.txt \ | ||
| -d '{ "email": "teammate@example.com", "role": "member" }' | ||
| ``` | ||
PierreBrisorgueil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The invitee then accepts (or declines) via the invite token they receive | ||
| by email: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/api/invites/<token>/accept \ | ||
| -b cookies.txt | ||
| ``` | ||
|
|
||
| ## Scoping requests to an organization | ||
|
|
||
| The API does not use an `X-Organization-Id` header. Org context is | ||
| resolved in one of two ways: | ||
|
|
||
| 1. **Route parameter** — org-scoped routes include `:organizationId` in | ||
| the path, e.g. `/api/organizations/:organizationId/invites`. Pass the | ||
| org id directly in the URL. | ||
| 2. **Current organization** — the authenticated user has a | ||
| `currentOrganization` stored server-side. Switch it with: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3000/api/organizations/<orgId>/switch \ | ||
| -b cookies.txt | ||
| ``` | ||
|
|
||
| This updates `user.currentOrganization`, issues a fresh JWT cookie, | ||
| and rebuilds abilities. Subsequent requests that rely on the current | ||
| org (rather than a route param) use that value. | ||
|
|
||
| ## Roles | ||
|
|
||
| | Role | Permissions | | ||
| |------|-------------| | ||
| | **owner** | Full access, manage billing, delete organization | | ||
| | **admin** | Manage members, update settings | | ||
| | **member** | Access shared resources | | ||
|
|
||
| Roles are enforced by CASL abilities on the backend — see each | ||
| organization endpoint for the required ability. | ||
|
|
||
| ## Next steps | ||
|
|
||
| - Browse the endpoint reference for the full list of organization routes. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.