feat: add workos-widgets skill with on-demand OpenAPI spec querying (#4)

* Add claude skill to create WorkOS widgets

* Add guidelines about styling existing ui components and where to implement the widget

* Optimize some skills

* Add list of endpoints to fetching api docs

* fix: resolve widget skill conflicts and route to hand-crafted skill

- Delete hallucinated generated widget files (fabricated workos.widgets.mount() API)
- Route widget requests via Skill tool instead of generated references
- Add workos-widgets to HAND_CRAFTED_SKILLS, skip widgets in SECTION_CONFIG
- Move OpenAPI spec from always-loaded to on-demand in SKILL.md
- Add error response documentation to fetching-apis.md
- Add strip-openapi-spec.ts script and strip boilerplate (3375 → 2774 lines)
- Fix paths.spec.ts and splitter.spec.ts for new config

* feat: add on-demand OpenAPI spec query script

Replace always-loaded/stripped spec with a query script that extracts
just the endpoints and resolved schemas for a specific widget.

- Add references/scripts/query-spec.ts (--widget, --path, --list modes)
- Revert spec to original unstripped version
- Update SKILL.md to reference query script instead of raw spec
- Remove strip-openapi-spec.ts (no longer needed)

* feat: add query-spec tests and fix  resolution in arrays

- Add 36 tests for query-spec (resolveRef, resolveSchema, extractEndpoints,
  formatEndpoint, groupPathsByWidget, WIDGET_PREFIXES, integration)
- Fix resolveSchema to recursively resolve $refs inside arrays (oneOf, anyOf)
- Revert OpenAPI spec to original unstripped version
- Remove strip-openapi-spec.ts

* feat: bundle query-spec as self-contained .cjs for portability

- Add esbuild to bundle query-spec.ts → query-spec.cjs (includes yaml dep)
- Update SKILL.md to use 'node references/scripts/query-spec.cjs' (no tsx needed)
- Add build:query-spec script to package.json
- Fix __dirname resolution to support both ESM and CJS contexts

* chore: add .oxlintrc and ignore built file in it and oxfmt

* fix: point fetching-apis.md references to query script, remove redundant SKILL.md mention

* chore: remove unused strip-openapi-spec.ts

Superseded by the on-demand query-spec approach.

* fix: address final review findings

- token-strategies.md: point elevated access to fetching-apis.md markers
  instead of raw spec, use explicit verify endpoint path
- fetching-apis.md: update objective line, hardcode base URL fallback
  to https://api.workos.com instead of referencing spec
- query-spec.ts: add SKILL.md slug aliases (user-management,
  user-profile, admin-portal-sso-connection, admin-portal-domain-verification)
- query-spec.ts: update source comments to reference .cjs bundle
- Rebuild query-spec.cjs

---------

Co-authored-by: Lucas Motta <mail@lucasmotta.com>
This commit is contained in:
Nick Nisi
2026-03-06 14:07:46 -06:00
committed by GitHub
parent 874f809808
commit 39269e0698
38 changed files with 12441 additions and 282 deletions
+1 -1
View File
@@ -6,5 +6,5 @@
"singleQuote": true,
"printWidth": 120,
"sortPackageJson": false,
"ignorePatterns": ["dist/", "node_modules/", "pnpm-lock.yaml", "CHANGELOG.md"]
"ignorePatterns": ["dist/", "node_modules/", "pnpm-lock.yaml", "CHANGELOG.md", "query-spec.cjs"]
}
+40
View File
@@ -0,0 +1,40 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": null,
"categories": {},
"rules": {},
"settings": {
"jsx-a11y": {
"polymorphicPropName": null,
"components": {},
"attributes": {}
},
"next": {
"rootDir": []
},
"react": {
"formComponents": [],
"linkComponents": [],
"version": null,
"componentWrapperFunctions": []
},
"jsdoc": {
"ignorePrivate": false,
"ignoreInternal": false,
"ignoreReplacesDocs": true,
"overrideReplacesDocs": true,
"augmentsExtendsReplacesDocs": false,
"implementsReplacesDocs": false,
"exemptDestructuredRootsFromChecks": false,
"tagNamePreference": {}
},
"vitest": {
"typecheck": false
}
},
"env": {
"builtin": true
},
"globals": {},
"ignorePatterns": ["query-spec.cjs"]
}
+3 -1
View File
@@ -29,13 +29,15 @@
"format": "oxfmt .",
"format:check": "prettier --check .",
"lint": "oxlint",
"lint:fix": "oxlint --fix"
"lint:fix": "oxlint --fix",
"build:query-spec": "esbuild plugins/workos/skills/workos-widgets/references/scripts/query-spec.ts --bundle --platform=node --format=cjs --outfile=plugins/workos/skills/workos-widgets/references/scripts/query-spec.cjs"
},
"dependencies": {
"yaml": "^2.8.2"
},
"devDependencies": {
"@types/node": "^22.0.0",
"esbuild": "^0.27.3",
"oxfmt": "^0.35.0",
"oxlint": "^1.50.0",
"tsx": "^4.19.0",
@@ -0,0 +1,127 @@
---
name: workos-widgets
description: Build, integrate, or migrate WorkOS Widgets in modern web apps. Use this skill when implementing User Management, User Profile, Admin Portal SSO Connection, or Admin Portal Domain Verification widgets across Next.js, React Router, TanStack Router, TanStack Start, Vite, SvelteKit, Ruby, Python, Go, PHP, or Java stacks. Detect the active stack, auth/token strategy, data-layer style, and UI conventions; then implement widget integration with correct access-token flow and API calls based on the bundled Widgets OpenAPI spec.
---
# WorkOS Widgets
## Workflow Overview
1. Identify widget target from the user request (`user-management`, `user-profile`, `admin-portal-sso-connection`, `admin-portal-domain-verification`).
2. Scan project files in this order:
- package/dependency manifests
- framework/router entrypoints
- auth/token utilities
- styling/component patterns
3. Detect stack, data-layer style, styling, component system, and package manager using [references/detection.md](references/detection.md).
4. Check for AuthKit/WorkOS presence:
- if detected, continue;
- if not detected, ask the user to run `npx workos@latest install`, wait for confirmation, then continue.
5. If detection is ambiguous or conflicting, ask one focused question, then continue.
6. Load only the relevant reference files for the detected stack and widget.
7. Implement integration based on stack shape:
- frontend route/page + widget component when widget UI lives in the same app
- token endpoint/service + client integration surface when backend-first/multi-app architecture is detected
8. Validate routing/wiring, imports, and token/API usage before finishing.
## Canonical Inputs
Accept these inputs from the user request when available:
- widget type (or infer from request intent)
- optional component path
- optional page/route path
- optional token endpoint/service preference
- optional constraints (for example: avoid broad refactors)
When input is missing, infer from existing project conventions and detected stack.
## Detection and Ambiguity Protocol
- Apply detection heuristics from [references/detection.md](references/detection.md).
- Explore before asking. Ask only when ambiguity remains after checking manifests and route/auth entrypoints.
- Ask a single concrete question that resolves one decision.
- Default to the strongest detected ownership signals when no user response is available.
- When installs are required, use the package manager detected from project files/lockfiles.
## Reference Loading Map
Always load these core references:
- [references/detection.md](references/detection.md)
- [references/token-strategies.md](references/token-strategies.md)
- [references/fetching-apis.md](references/fetching-apis.md)
- [references/styling-and-components.md](references/styling-and-components.md)
For React/TypeScript stacks (Next.js, React Router, TanStack Router, TanStack Start, Vite), also load:
- [references/react-ts-standards.md](references/react-ts-standards.md)
Load stack-specific reference guidance:
- Next.js: [references/framework-nextjs.md](references/framework-nextjs.md)
- React Router: [references/framework-react-router.md](references/framework-react-router.md)
- TanStack Router: [references/framework-tanstack-router.md](references/framework-tanstack-router.md)
- TanStack Start: [references/framework-tanstack-start.md](references/framework-tanstack-start.md)
- Vite: [references/framework-vite.md](references/framework-vite.md)
- SvelteKit: [references/framework-sveltekit.md](references/framework-sveltekit.md)
- Ruby: [references/framework-ruby.md](references/framework-ruby.md)
- Python: [references/framework-python.md](references/framework-python.md)
- Go: [references/framework-go.md](references/framework-go.md)
- PHP: [references/framework-php.md](references/framework-php.md)
- Java: [references/framework-java.md](references/framework-java.md)
- Mixed repositories: [references/framework-mixed-repositories.md](references/framework-mixed-repositories.md)
Then load exactly one widget reference:
- User Management: [references/widget-user-management.md](references/widget-user-management.md)
- User Profile: [references/widget-user-profile.md](references/widget-user-profile.md)
- Admin Portal SSO Connection: [references/widget-admin-portal-sso-connection.md](references/widget-admin-portal-sso-connection.md)
- Admin Portal Domain Verification: [references/widget-admin-portal-domain-verification.md](references/widget-admin-portal-domain-verification.md)
## Global Widget Guidance
- Implement widget operations using endpoint paths/methods from [references/fetching-apis.md](references/fetching-apis.md). When building request bodies or parsing responses, query the OpenAPI spec for the relevant widget's schemas:
```bash
node references/scripts/query-spec.cjs --widget <widget-name>
```
Use `--list` to see available widget groups.
- Keep loading, empty, and error states explicit and user-visible.
- Keep mutation outcomes visible and refresh/reload affected data after successful changes.
- Align table/list/action UI with existing project conventions.
- Keep behavior resilient for partial/optional data and avoid brittle UI assumptions.
## Core Guidelines
- Reuse existing domain types from the host project and OpenAPI schemas; avoid duplicating model definitions.
- Build widget requests using [references/fetching-apis.md](references/fetching-apis.md) for paths, methods, and schema queries.
- Use direct `fetch`/HTTP calls (or equivalent server HTTP client) for endpoint calls.
- Implement a consistent authorization layer for widget requests, including elevated-token handling for sensitive endpoints when required.
- If the app already uses React Query or SWR, use them as orchestration/cache layers around those direct calls.
- For React/TypeScript widget code quality expectations, follow [references/react-ts-standards.md](references/react-ts-standards.md).
- If AuthKit/WorkOS is missing, prompt the user to run `npx workos@latest install` before continuing.
- Install additional dependencies only when strictly necessary, using the detected package manager/tooling.
- Keep server-state handling aligned with the selected data-layer approach.
- Use local state/reducers for UI interaction state as needed.
- Prefer existing design system and styling conventions.
- Avoid broad unrelated refactors and global style rewrites.
## Completion Requirements
Before finishing, verify all relevant items:
1. Widget component exists and accepts `accessToken: string` when component-level integration is in scope.
2. Route/page wiring is complete when route integration is in scope.
3. Token source matches existing app architecture (AuthKit client flow or backend WorkOS token flow).
4. API methods and paths match the bundled OpenAPI spec, and data-layer usage matches project conventions.
5. Loading and error branches exist for required query/mutation flows.
## Validation Checklist
1. Confirm endpoint paths and HTTP methods come from the bundled OpenAPI spec.
2. Confirm request/response handling follows schema expectations from the spec.
3. Confirm query/mutation invalidation/refetch is applied after successful mutations where required.
4. Confirm empty/error/loading states are explicit and user-visible.
5. Confirm package installs (if any) used the detected package manager/tooling.
6. Confirm implementation stays aligned with existing codebase conventions.
7. Confirm no existing component has been passed `className` or `style` props to override its built-in styling. Use each component as-is or via its own props API (`variant`, `size`, etc.).
@@ -0,0 +1,89 @@
# Detection
## Objective
Identify the active stack and integration surface using repository signals, then choose an approach that fits existing architecture.
## Suggested Scan Order
1. dependency manifests (`package.json`, `Gemfile`, `composer.json`, `pyproject.toml`, `requirements*.txt`, `go.mod`, `pom.xml`, `build.gradle*`)
2. framework/router entrypoints
3. auth and token utilities
4. styling and component patterns
5. package manager and lockfiles
## Common Stack Signals
### JavaScript/TypeScript
- Next.js: `next`
- TanStack Start: `@tanstack/react-start`
- TanStack Router: `@tanstack/react-router`
- React Router: `react-router` or `react-router-dom`
- Vite: `vite` or `vite.config.*`
- SvelteKit: `@sveltejs/kit` or `svelte.config.*`
### Other Stacks
- Ruby: `Gemfile` and WorkOS/AuthKit gems
- PHP: `composer.json` and WorkOS/AuthKit packages
- Python: `pyproject.toml` or `requirements*.txt` with WorkOS/AuthKit packages
- Go: `go.mod` with `github.com/workos/workos-go`
- Java: `pom.xml` or `build.gradle*` with WorkOS dependencies/imports
## AuthKit/WorkOS Presence Signals
Look for any existing AuthKit/WorkOS usage before implementing widgets.
- JavaScript/TypeScript: `@workos-inc/*`, `@workos/*`, or WorkOS/AuthKit imports in app/server code
- Ruby: WorkOS/AuthKit gems or initialization code
- PHP: WorkOS/AuthKit composer packages or bootstrap usage
- Python: WorkOS/AuthKit packages/imports and config usage
- Go: WorkOS Go module imports/config
- Java: WorkOS Java dependency/imports/config
If no AuthKit/WorkOS signal is found, see SKILL.md step 4.
## Detection Heuristics
- Prefer entrypoint ownership over dependency names alone.
- In mixed repositories, identify which app owns UI rendering and which service owns token generation.
- Use the strongest cluster of signals, then validate by checking real route/auth files.
## Data-Layer Signals
- React Query signal: `@tanstack/react-query`
- SWR signal: `swr`
- No query library signal: use the project's native async/data approach with direct fetch/http calls.
When React Query or SWR is already established, keep using it for caching/invalidation and wrap direct endpoint calls with it.
## Package Manager/Tool Detection
Use the project's existing package manager/tooling when installing missing dependencies.
- JavaScript/TypeScript:
- `pnpm-lock.yaml` -> `pnpm`
- `yarn.lock` -> `yarn`
- `bun.lockb` or `bun.lock` -> `bun`
- `package-lock.json` -> `npm`
- if unclear, use existing install scripts or ask once
- Ruby: use `bundle`/Bundler with `Gemfile`
- PHP: use `composer` with `composer.json`
- Python: follow existing tooling (`poetry.lock`, `uv.lock`, `requirements*.txt`, or existing project scripts)
- Go: use Go modules tooling (`go mod` / existing project scripts)
- Java: follow project tooling (`mvn`/Maven or `gradle`/Gradle wrappers)
Install dependencies only when strictly necessary for the selected integration approach.
## Ambiguity Handling
- If multiple stacks/frameworks look active, ask one focused question.
- If ownership is still unclear, choose the least invasive path and note assumptions.
## Focused Question Examples
- "I found multiple routers. Which one currently owns app routes?"
- "I found backend and frontend apps. Which app should host the widget UI?"
- "I found multiple data-fetching patterns. Which one should new widget code follow?"
- "I found multiple services that could issue widget tokens. Which one should own token generation?"
@@ -0,0 +1,129 @@
# Fetching APIs
## Objective
Implement Widgets API calls using the endpoint tables and query script below, matching the host application's data layer.
## Source of Truth
Use the endpoint tables below for paths and methods. For request/response schemas, run:
```bash
node references/scripts/query-spec.cjs --widget <widget-name>
```
## Guidance
- Build direct fetch/http client functions from the OpenAPI endpoints.
- Keep request and mutation handling consistent with existing code style.
- If React Query or SWR already exists, use it for query/mutation orchestration on top of the direct endpoint functions.
- Prefer one consistent data pattern per widget flow unless the project already mixes patterns.
- Reuse existing error/loading conventions from the host project.
## Base URL
Use `process.env.WORKOS_BASE_API_URL` (or the equivalent env access for the stack) as the base URL for all widget API calls. Fall back to `https://api.workos.com` when the env variable is not set.
## Authorization Layer
- Add a small shared request layer that injects authorization consistently for all widget calls.
- Send the widget bearer token in the app's standard authenticated request path.
- Keep authorization wiring close to existing auth/session utilities instead of duplicating token logic across components.
- Handle `401`/`403` responses explicitly and surface clear recovery actions.
## Error Responses
All error responses (`400`, `403`, `404`, `422`) return a JSON object with a single `message` string field:
```json
{ "message": "Description of the error" }
```
For full request/response schemas, run `node references/scripts/query-spec.cjs --widget <widget-name>`.
## Elevated Access Endpoints
- Check the endpoint's description (via `node references/scripts/query-spec.cjs --widget <widget-name>`) **before calling it** — not on failure. If it mentions elevated access, acquire the elevated token first.
- Use `POST /_widgets/UserProfile/verify` to obtain an elevated token, then pass it in header `x-elevated-access-token`.
- Treat elevated tokens as short-lived (10 minutes) and scope them to sensitive operations only.
## Endpoint Reference
All available endpoints, grouped by widget. For request/response schemas, run `node references/scripts/query-spec.cjs --widget <widget-name>`.
### User Management
| Method | Path |
| -------- | -------------------------------------------------- |
| `GET` | `/_widgets/UserManagement/members` |
| `POST` | `/_widgets/UserManagement/members/{userId}` |
| `DELETE` | `/_widgets/UserManagement/members/{userId}` |
| `GET` | `/_widgets/UserManagement/roles` |
| `GET` | `/_widgets/UserManagement/roles-and-config` |
| `GET` | `/_widgets/UserManagement/organizations` |
| `POST` | `/_widgets/UserManagement/invite-user` |
| `POST` | `/_widgets/UserManagement/invites/{userId}/resend` |
| `DELETE` | `/_widgets/UserManagement/invites/{userId}` |
### User Profile
| Method | Path |
| -------- | -------------------------------------------------------- |
| `GET` | `/_widgets/UserProfile/me` |
| `POST` | `/_widgets/UserProfile/me` |
| `GET` | `/_widgets/UserProfile/authentication-information` |
| `POST` | `/_widgets/UserProfile/send-verification` |
| `POST` | `/_widgets/UserProfile/verify` |
| `POST` | `/_widgets/UserProfile/update-password` |
| `POST` | `/_widgets/UserProfile/create-password` ⚠️ elevated |
| `POST` | `/_widgets/UserProfile/create-totp-factor` ⚠️ elevated |
| `POST` | `/_widgets/UserProfile/verify-totp-factor` ⚠️ elevated |
| `DELETE` | `/_widgets/UserProfile/totp-factors` ⚠️ elevated |
| `POST` | `/_widgets/UserProfile/passkeys` ⚠️ elevated |
| `POST` | `/_widgets/UserProfile/passkeys/verify` ⚠️ elevated |
| `DELETE` | `/_widgets/UserProfile/passkeys/{passkeyId}` ⚠️ elevated |
| `GET` | `/_widgets/UserProfile/sessions` |
| `DELETE` | `/_widgets/UserProfile/sessions/revoke/{sessionId}` |
| `DELETE` | `/_widgets/UserProfile/sessions/revoke-all` |
### Admin Portal — SSO Connection
| Method | Path |
| ------ | ---------------------------------------- |
| `GET` | `/_widgets/admin-portal/sso-connections` |
| `POST` | `/_widgets/admin-portal/generate-link` |
### Admin Portal — Domain Verification
| Method | Path |
| -------- | ----------------------------------------------------------------- |
| `GET` | `/_widgets/admin-portal/organization-domains` |
| `DELETE` | `/_widgets/admin-portal/organization-domains/{domainId}` |
| `POST` | `/_widgets/admin-portal/organization-domains/{domainId}/reverify` |
| `POST` | `/_widgets/admin-portal/generate-link` |
### Other
| Method | Path |
| -------- | ----------------------------------------------------------------------------- |
| `GET` | `/_widgets/settings` |
| `POST` | `/_widgets/ApiKeys/organization-api-keys` |
| `GET` | `/_widgets/ApiKeys/organization-api-keys` |
| `GET` | `/_widgets/ApiKeys/permissions` |
| `DELETE` | `/_widgets/ApiKeys/{apiKeyId}` |
| `GET` | `/_widgets/DataIntegrations/mine` |
| `GET` | `/_widgets/DataIntegrations/{slug}/authorize` |
| `GET` | `/_widgets/DataIntegrations/{dataIntegrationId}/authorization-status/{state}` |
| `DELETE` | `/_widgets/DataIntegrations/installations/{installationId}` |
| `GET` | `/_widgets/directory-sync/directories` |
| `GET` | `/_widgets/directory-sync/directories/{directoryId}` |
## Pagination
List endpoints use cursor-based pagination. Query parameters:
- `limit` — number of results per page
- `before` — cursor for the previous page
- `after` — cursor for the next page
Responses include a `list_metadata` object with `before` and `after` cursor strings. Pass `after` from the current response as the `after` param of the next request to advance pages.
@@ -0,0 +1,34 @@
# Framework: Go
## Scope
Use this guide for Go services that issue widget tokens and support widget API integration.
## Guidance
- Use the official WorkOS Go SDK.
- Keep API key in environment configuration.
- Place token generation in existing handler/service layers.
- Reuse existing auth/session middleware to derive organization/user identifiers.
## Token Pattern
```go
import (
"context"
"os"
"github.com/workos/workos-go/v4/pkg/widgets"
)
widgets.SetAPIKey(os.Getenv("WORKOS_API_KEY"))
token, err := widgets.GetToken(
context.Background(),
widgets.GetTokenOpts{
OrganizationID: organizationID,
UserID: userID,
Scopes: []widgets.WidgetScope{widgets.UsersTableManage},
},
)
```
@@ -0,0 +1,32 @@
# Framework: Java
## Scope
Use this guide for Java services/apps that issue widget tokens and support widget integrations.
## Guidance
- Use the official WorkOS Java SDK.
- Keep API key in environment configuration.
- Place token creation in existing service/controller boundaries.
- Reuse existing auth/session context for organization and user identifiers.
## Token Pattern
```java
import com.workos.WorkOS;
import com.workos.widgets.WidgetsApi.GetTokenOptions;
import com.workos.widgets.models.WidgetScope;
import com.workos.widgets.models.WidgetTokenResponse;
WorkOS workos = new WorkOS(System.getenv("WORKOS_API_KEY"));
GetTokenOptions options = GetTokenOptions.builder()
.organizationID(organizationId)
.userID(userId)
.scopes(Arrays.asList(WidgetScope.WidgetsUsersTableManage))
.build();
WidgetTokenResponse response = workos.widgets.getToken(options);
String token = response.token;
```
@@ -0,0 +1,13 @@
# Framework: Mixed Repositories
## Objective
Handle repositories with multiple apps/services by integrating widgets at existing boundaries.
## Guidance
- Detect which app owns widget UI rendering.
- Detect which service owns authenticated token generation.
- Keep each side in its native conventions and integrate through existing API boundaries.
- Avoid broad architecture moves when additive wiring is enough.
- If unsure, prompt the user.
@@ -0,0 +1,13 @@
# Framework: Next.js
## Guidance
- Detect whether the project uses App Router or Pages Router, then follow that structure.
- Place widget routes/pages where existing route modules live.
- Keep token acquisition in the same server/client boundary already used by the app.
- For JS/TS token strategy details (AuthKit token vs backend `getToken` with scopes), follow [token-strategies.md](token-strategies.md).
- Integrate widget components through existing layout and provider patterns.
## Server Token Pattern (JS/TS)
For the token code pattern, see [token-strategies.md](token-strategies.md) → JS/TS Authorization Tokens. Token generation belongs in a Next.js server boundary (Server Component, Route Handler, or `getServerSideProps`).
@@ -0,0 +1,29 @@
# Framework: PHP
## Scope
Use this guide for PHP apps (for example Laravel/Symfony) that create widget tokens and integrate widget APIs.
## Guidance
- Use the official WorkOS PHP SDK.
- Keep API key in environment configuration.
- Place token generation in existing controller/service boundaries.
- Reuse current auth/session context to resolve organization/user identifiers.
## Token Pattern
```php
<?php
use WorkOS\Resource\WidgetScope;
WorkOS\WorkOS::setApiKey($_ENV['WORKOS_API_KEY']);
$widgets = new WorkOS\Widgets();
$token_response = $widgets->getToken(
organization_id: $organizationId,
user_id: $userId,
scopes: [WidgetScope::UsersTableManage]
);
```
@@ -0,0 +1,29 @@
# Framework: Python
## Scope
Use this guide for Python apps (for example Django/Flask/FastAPI) that generate widget tokens and/or broker widget API requests.
## Guidance
- Use the official WorkOS Python SDK.
- Keep API key and client id in environment configuration.
- Place token generation in existing service/view/router boundaries.
- Reuse established auth/session context for `organization_id` and `user_id`.
## Token Pattern
```py
from workos import WorkOSClient
workos_client = WorkOSClient(
api_key=os.environ["WORKOS_API_KEY"],
client_id=os.environ["WORKOS_CLIENT_ID"],
)
token_response = workos_client.widgets.get_token(
organization_id=organization_id,
user_id=user_id,
scopes=["widgets:users-table:manage"],
)
```
@@ -0,0 +1,13 @@
# Framework: React Router
## Guidance
- Follow the repository's route definition style (file-based or config-based).
- Add widget routes/components in the same structure used by existing features.
- Reuse existing loader/action or component-level token patterns.
- For JS/TS token strategy details (AuthKit token vs backend `getToken` with scopes), follow [token-strategies.md](token-strategies.md).
- Preserve current router/provider setup and conventions.
## Server Token Pattern (JS/TS)
For the token code pattern, see [token-strategies.md](token-strategies.md) → JS/TS Authorization Tokens. Token generation belongs in a loader, action, or dedicated server route.
@@ -0,0 +1,28 @@
# Framework: Ruby
## Scope
Use this guide for Ruby apps (for example Rails/Sinatra) that generate widget tokens and/or proxy widget API calls.
## Guidance
- Use the official WorkOS Ruby SDK.
- Keep API key in environment configuration.
- Place token generation in existing service/controller boundaries.
- Reuse existing session/auth context to resolve `organization_id` and `user_id`.
## Token Pattern
```rb
require "workos"
WorkOS.configure do |config|
config.key = ENV.fetch("WORKOS_API_KEY")
end
token = WorkOS::Widgets.get_token(
organization_id: organization_id,
user_id: user_id,
scopes: ["widgets:users-table:manage"]
)
```
@@ -0,0 +1,13 @@
# Framework: SvelteKit
## Guidance
- Follow existing `+page`, `+layout`, and `+server`/`+page.server` conventions.
- Keep token generation in server/load boundaries that already handle auth/session context.
- For JS/TS token strategy details (AuthKit token vs backend `getToken` with scopes), follow [token-strategies.md](token-strategies.md).
- Keep frontend data calls aligned with current SvelteKit patterns.
- Never embed a widget directly in a `+page.svelte`. Always extract it into its own `.svelte` component file. The page imports and renders that component.
## Server Token Pattern (JS/TS)
For the token code pattern, see [token-strategies.md](token-strategies.md) → JS/TS Authorization Tokens. Token generation belongs in a `+page.server.ts`, `+layout.server.ts`, or `+server.ts` boundary.
@@ -0,0 +1,13 @@
# Framework: TanStack Router
## Guidance
- Follow current route module conventions and route tree workflow.
- Place widget route files where the router expects them.
- Keep token retrieval aligned with existing loader/client boundaries.
- For JS/TS token strategy details (AuthKit token vs backend `getToken` with scopes), follow [token-strategies.md](token-strategies.md).
- Reuse existing typing and routing patterns from the project.
## Server Token Pattern (JS/TS)
For the token code pattern, see [token-strategies.md](token-strategies.md) → JS/TS Authorization Tokens. Token generation belongs in the route's loader or a dedicated server handler.
@@ -0,0 +1,13 @@
# Framework: TanStack Start
## Guidance
- Follow established Start route/file conventions.
- Keep server/client boundaries consistent with existing auth and data flows.
- Add widget integration with minimal structural changes.
- For JS/TS token strategy details (AuthKit token vs backend `getToken` with scopes), follow [token-strategies.md](token-strategies.md).
- Reuse current route and module organization patterns.
## Server Token Pattern (JS/TS)
For the token code pattern, see [token-strategies.md](token-strategies.md) → JS/TS Authorization Tokens. Token generation belongs in a Start server function or server boundary.
@@ -0,0 +1,13 @@
# Framework: Vite
## Guidance
- Detect whether routing is framework-based or custom, then integrate accordingly.
- Place widget pages/components in existing feature/page structure.
- Reuse existing token/auth utilities rather than introducing new architecture.
- For JS/TS token strategy details (AuthKit token vs backend `getToken` with scopes), follow [token-strategies.md](token-strategies.md).
- Keep integration small and aligned with current app layout.
## Server Token Pattern (JS/TS)
For the token code pattern, see [token-strategies.md](token-strategies.md) → JS/TS Authorization Tokens. In a Vite app, token generation typically lives in an existing backend service or API route rather than the Vite dev server.
@@ -0,0 +1,42 @@
# React/TypeScript Standards
## Objective
Keep React + TypeScript widget code predictable, type-safe, and easy to maintain.
## TypeScript Rules
- Prefer inference and explicit interface/type definitions over type assertions.
- Avoid `as` casts unless narrowing cannot be expressed safely another way.
- Avoid `any`; use concrete types or `unknown` with safe narrowing.
- Keep API response typing close to request functions and reuse shared domain types.
## React State and Hooks Rules
- Keep `useEffect` minimal and focused on true side effects.
- Do not use `useEffect` for derivable render data.
- Avoid `setState` inside `useEffect` unless syncing from external systems or subscriptions.
- Prefer deriving values from props/query state with memoization only when needed.
- Keep a single source of truth for server state (query/cache layer or explicit request state).
- Use local state for transient UI interactions only.
## Component Design Rules
- Keep components small and composable; extract repeated logic into hooks/utilities.
- Keep event handlers explicit and colocated with relevant UI.
- Avoid deep prop drilling when existing context/provider patterns already exist.
- Use clear loading/error/empty branches instead of implicit fallthrough behavior.
- Never embed a widget directly in a page. Always extract it into its own component file. The page imports and renders that component.
## Async and Mutation Rules
- Keep async request functions separate from view rendering logic.
- Prevent duplicate submits/actions while mutations are pending.
- Reflect mutation success/failure in UI state clearly.
- Refresh or invalidate affected data after successful mutations.
## General Code Quality
- Follow existing lint/format conventions in the host project.
- Prefer readable code over clever abstractions.
- Keep changes scoped to widget integration; avoid unrelated refactors.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,239 @@
/**
* Query the WorkOS Widgets OpenAPI spec for specific widget endpoints.
*
* Source file build with: pnpm build:query-spec
* Runtime usage (via bundled .cjs):
* node references/scripts/query-spec.cjs --widget UserProfile
* node references/scripts/query-spec.cjs --widget UserManagement
* node references/scripts/query-spec.cjs --widget admin-portal
* node references/scripts/query-spec.cjs --path /_widgets/UserProfile/me
* node references/scripts/query-spec.cjs --list
*
* Outputs matching endpoints with their request/response schemas (resolved $refs).
*/
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { parse } from 'yaml';
// Support both ESM (import.meta.url) and CJS (__dirname) for bundled output
const _dir = typeof __dirname !== 'undefined' ? __dirname : dirname(new URL(import.meta.url).pathname);
const SPEC_PATH = join(_dir, '../widgets-open-api-spec.yaml');
export const WIDGET_PREFIXES: Record<string, string> = {
// Primary names (match SKILL.md widget slugs)
'user-management': '/_widgets/UserManagement',
'user-profile': '/_widgets/UserProfile',
'admin-portal-sso-connection': '/_widgets/admin-portal/sso-connections',
'admin-portal-domain-verification': '/_widgets/admin-portal/organization-domains',
// Alternate forms
usermanagement: '/_widgets/UserManagement',
userprofile: '/_widgets/UserProfile',
'admin-portal': '/_widgets/admin-portal',
adminportal: '/_widgets/admin-portal',
'sso-connection': '/_widgets/admin-portal/sso-connections',
'domain-verification': '/_widgets/admin-portal/organization-domains',
apikeys: '/_widgets/ApiKeys',
dataintegrations: '/_widgets/DataIntegrations',
'directory-sync': '/_widgets/directory-sync',
settings: '/_widgets/settings',
};
export function loadSpec(path?: string) {
return parse(readFileSync(path ?? SPEC_PATH, 'utf-8'));
}
export function resolveRef(spec: Record<string, unknown>, ref: string): unknown {
const parts = ref.replace('#/', '').split('/');
let current: unknown = spec;
for (const part of parts) {
if (current && typeof current === 'object') {
current = (current as Record<string, unknown>)[part];
} else {
return undefined;
}
}
return current;
}
export function resolveSchema(spec: Record<string, unknown>, schema: unknown): unknown {
if (!schema || typeof schema !== 'object') return schema;
if (Array.isArray(schema)) {
return schema.map((item) => resolveSchema(spec, item));
}
const s = schema as Record<string, unknown>;
if (s.$ref && typeof s.$ref === 'string') {
return resolveSchema(spec, resolveRef(spec, s.$ref));
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(s)) {
if (value && typeof value === 'object') {
result[key] = resolveSchema(spec, value);
} else {
result[key] = value;
}
}
return result;
}
export interface EndpointInfo {
path: string;
method: string;
description?: string;
parameters?: unknown;
requestBody?: unknown;
responses: Record<string, unknown>;
}
export function extractEndpoints(spec: Record<string, unknown>, pathFilter: (path: string) => boolean): EndpointInfo[] {
const paths = spec.paths as Record<string, Record<string, unknown>> | undefined;
if (!paths) return [];
const endpoints: EndpointInfo[] = [];
for (const [path, methods] of Object.entries(paths)) {
if (!pathFilter(path)) continue;
for (const [method, operation] of Object.entries(methods)) {
if (!operation || typeof operation !== 'object') continue;
const op = operation as Record<string, unknown>;
const responses: Record<string, unknown> = {};
const opResponses = op.responses as Record<string, unknown> | undefined;
if (opResponses) {
for (const [code, resp] of Object.entries(opResponses)) {
const r = resp as Record<string, unknown>;
const content = r?.content as Record<string, unknown> | undefined;
const json = content?.['application/json'] as Record<string, unknown> | undefined;
if (json?.schema) {
responses[code] = {
description: r.description,
schema: resolveSchema(spec, json.schema),
};
} else {
responses[code] = { description: r?.description };
}
}
}
const endpoint: EndpointInfo = {
path,
method: method.toUpperCase(),
responses,
};
if (op.description) endpoint.description = op.description as string;
if (op.parameters) endpoint.parameters = op.parameters;
if (op.requestBody) {
const body = op.requestBody as Record<string, unknown>;
const content = body.content as Record<string, unknown> | undefined;
const json = content?.['application/json'] as Record<string, unknown> | undefined;
if (json?.schema) {
endpoint.requestBody = resolveSchema(spec, json.schema);
}
}
endpoints.push(endpoint);
}
}
return endpoints;
}
export function formatEndpoint(ep: EndpointInfo): string {
const lines: string[] = [];
lines.push(`## ${ep.method} ${ep.path}`);
if (ep.description) lines.push(`\n${ep.description}`);
if (ep.parameters && Array.isArray(ep.parameters) && ep.parameters.length > 0) {
lines.push('\n### Parameters\n');
for (const p of ep.parameters) {
const param = p as Record<string, unknown>;
lines.push(`- \`${param.name}\` (${param.in}): ${param.schema ? JSON.stringify(param.schema) : 'unknown'}`);
}
}
if (ep.requestBody) {
lines.push('\n### Request Body\n');
lines.push('```json');
lines.push(JSON.stringify(ep.requestBody, null, 2));
lines.push('```');
}
for (const [code, resp] of Object.entries(ep.responses)) {
const r = resp as Record<string, unknown>;
lines.push(`\n### Response ${code}${r.description ? `${r.description}` : ''}\n`);
if (r.schema) {
lines.push('```json');
lines.push(JSON.stringify(r.schema, null, 2));
lines.push('```');
}
}
return lines.join('\n');
}
export function groupPathsByWidget(paths: string[]): Record<string, string[]> {
const grouped: Record<string, string[]> = {};
for (const p of paths) {
const parts = p.split('/').filter(Boolean);
const group = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0] || 'other';
if (!grouped[group]) grouped[group] = [];
grouped[group].push(p);
}
return grouped;
}
// --- CLI (only runs when executed directly) ---
const isDirectExecution = process.argv[1]?.includes('query-spec');
if (isDirectExecution) {
const args = process.argv.slice(2);
if (args.includes('--list')) {
const spec = loadSpec();
const grouped = groupPathsByWidget(Object.keys(spec.paths ?? {}));
for (const [group, groupPaths] of Object.entries(grouped)) {
console.log(`\n${group}:`);
for (const p of groupPaths) console.log(` ${p}`);
}
process.exit(0);
}
const widgetIdx = args.indexOf('--widget');
const pathIdx = args.indexOf('--path');
if (widgetIdx === -1 && pathIdx === -1) {
console.error('Usage: query-spec.ts --widget <name> | --path <path> | --list');
console.error('Widgets:', Object.keys(WIDGET_PREFIXES).join(', '));
process.exit(1);
}
const spec = loadSpec();
let filter: (path: string) => boolean;
if (widgetIdx !== -1) {
const widgetName = (args[widgetIdx + 1] || '').toLowerCase();
const prefix = WIDGET_PREFIXES[widgetName];
if (!prefix) {
console.error(`Unknown widget: ${args[widgetIdx + 1]}`);
console.error('Known widgets:', Object.keys(WIDGET_PREFIXES).join(', '));
process.exit(1);
}
filter = (path) => path.startsWith(prefix);
} else {
const pathPattern = args[pathIdx + 1] || '';
filter = (path) => path.includes(pathPattern);
}
const endpoints = extractEndpoints(spec, filter);
if (endpoints.length === 0) {
console.error('No matching endpoints found.');
process.exit(1);
}
console.log(`# Matched ${endpoints.length} endpoint(s)\n`);
for (const ep of endpoints) {
console.log(formatEndpoint(ep));
console.log('\n---\n');
}
}
@@ -0,0 +1,25 @@
# Styling and Components
## Objective
Match widget UI to the host application's existing component and styling systems.
## Do
- Use detected styling approach (Tailwind, CSS, CSS Modules, SCSS, styled-components, Emotion).
- Prefer existing shared UI components before introducing new primitives.
- Keep naming and file placement aligned with existing conventions.
- Add only minimal new styles required for widget UX.
## Don't
- Don't introduce a new styling system when one is already established; if in doubt, confirm with the user.
- Don't break or modify existing styles outside widget integration scope.
- Don't break established spacing/typography conventions.
- Don't override existing component styles by passing `className` or `style` props to components that already have their own styling. Use `<Button>` as-is, never `<Button className="bg-red-500">`. If customization is needed, use the component's own API (e.g. `variant`, `size` props).
## Component System Notes
- shadcn: use existing shadcn components; add missing components through shadcn CLI only when required by widget behavior.
- radix/base-ui/react-aria/ariakit/ark-ui: follow current primitive and composition conventions already used in the repo.
- custom/none: implement simple, maintainable UI with the detected styling system and avoid overengineering.
@@ -0,0 +1,65 @@
# Token Strategies
## Objective
Provide `accessToken` to widget surfaces using the app's existing auth architecture.
## Guidance
- Prefer existing AuthKit/session flows when they are already established.
- If backend token creation already exists, follow that pattern.
- Keep token-related logic near current auth boundaries.
- Pass token values explicitly into widget entry surfaces.
- Send the widget token through the app's existing authenticated HTTP pattern when calling widget endpoints.
- Use environment variables for credentials/config instead of hardcoded keys.
- For endpoints that require elevated access, follow the elevation flow and handle elevated token usage separately from the regular widget token.
## Widget Scope Reference
Use the scope that matches the widget being implemented:
| Widget | Required Scope |
| ---------------------------------- | ------------------------------------ |
| `user-management` | `widgets:users-table:manage` |
| `user-profile` | _(no permission scope required)_ |
| `admin-portal-sso-connection` | `widgets:sso:manage` |
| `admin-portal-domain-verification` | `widgets:domain-verification:manage` |
## JS/TS Authorization Tokens
Widgets need an authorization token and JS/TS apps typically use one of two paths:
1. If the app uses `authkit-js` or `authkit-react`, use the existing access token flow.
2. If the app uses a backend WorkOS SDK, request a widget token with `workos.widgets.getToken(...)` and the scope for the selected widget (see Widget Scope Reference above).
Widget tokens expire after one hour.
```ts
const workos = new WorkOS(process.env.WORKOS_API_KEY, {
clientId: process.env.WORKOS_CLIENT_ID,
// Use WORKOS_BASE_API_URL if set (e.g. for staging/local); falls back to default
...(process.env.WORKOS_BASE_API_URL && { host: process.env.WORKOS_BASE_API_URL }),
});
const authToken = await workos.widgets.getToken({
userId: user.id,
organizationId,
scopes: ['<scope-for-this-widget>'], // see Widget Scope Reference above
});
```
To generate a token successfully, the user needs a role with the required widget permissions. When token generation fails due to authorization, check role permissions in the WorkOS Dashboard roles configuration.
New WorkOS accounts typically start with an Admin role that already has widget permissions. Existing accounts may need explicit role permission updates. Reference: [Roles and Permissions guide](https://workos.com/docs/authkit/roles-and-permissions).
## Elevated Access Tokens
Some operations require elevated access in addition to the normal widget token. Check the endpoint table in `fetching-apis.md` for the ⚠️ elevated marker **before calling it** — not on failure. If marked elevated, acquire the elevated token first.
1. Use the `POST /_widgets/UserProfile/verify` endpoint to obtain an elevated access token.
2. Use the returned token (`elevatedAccessToken`) in request header `x-elevated-access-token`.
3. Treat elevated tokens as short-lived credentials (10 minutes) and scope usage to sensitive action paths only.
## Example Direction
When backend WorkOS SDK usage is present, use its existing token creation path and adapt it for the required widget scope.
@@ -0,0 +1,15 @@
# Widget: Admin Portal Domain Verification
## Purpose
The Admin Portal Domain Verification widget lets users verify domains through Admin Portal workflows.
## Permission Requirement
The acting user should have a role with `widgets:domain-verification:manage`.
## Complete When
- The widget lists domains with: domain value, added date/time, and verification status.
- The widget supports actions to restart verification and remove domain.
- The widget includes an "Add domain" action that links to Admin Portal domain management.
@@ -0,0 +1,14 @@
# Widget: Admin Portal SSO Connection
## Purpose
The Admin Portal SSO Connection widget lets users set up and manage SSO connections in Admin Portal.
## Permission Requirement
The acting user should have a role with `widgets:sso:manage`.
## Complete When
- The widget shows connection name and connection status.
- The widget includes a "Manage connection" action that links to Admin Portal.
@@ -0,0 +1,18 @@
# Widget: User Management
## Purpose
The User Management widget lets organization admins manage members in an organization.
## Permission Requirement
The acting user should have a role with `widgets:users-table:manage`.
## Complete When
- The widget shows a paginated members table.
- The table includes: avatar, name, email, role, last active time, and status when available.
- Search is implemented server-side.
- Role filtering is implemented server-side.
- Per-user actions are available for editing role and deleting/removing user.
- Admin invite flow is available for adding new users.
@@ -0,0 +1,11 @@
# Widget: User Profile
## Purpose
The User Profile widget lets end users view and manage their personal information.
## Complete When
- The widget displays profile picture, name, email, and connected accounts.
- The widget includes an action to edit the user's name.
- Profile updates reflect success/failure clearly in UI.
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -29,13 +29,13 @@ When a user needs help with WorkOS, consult the tables below to route to the rig
| Install AuthKit with TanStack Start | workos-authkit-tanstack-start |
| Install AuthKit in vanilla JS | workos-authkit-vanilla-js |
| AuthKit architecture reference | workos-authkit-base |
| Add WorkOS Widgets | workos-widgets |
### Features (Read `references/{name}.md`)
| User wants to... | Read file |
| ------------------------------- | ------------------------------------- |
| Configure email delivery | `references/workos-email.md` |
| Add WorkOS Widgets | `references/workos-widgets.md` |
| Encrypt data with Vault | `references/workos-vault.md` |
| Configure Single Sign-On | `references/workos-sso.md` |
| Implement RBAC / roles | `references/workos-rbac.md` |
@@ -60,7 +60,6 @@ When a user needs help with WorkOS, consult the tables below to route to the rig
| Roles API Reference | `references/workos-api-roles.md` |
| Sso API Reference | `references/workos-api-sso.md` |
| Vault API Reference | `references/workos-api-vault.md` |
| Widgets API Reference | `references/workos-api-widgets.md` |
### Migrations (Read `references/{name}.md`)
@@ -95,7 +94,7 @@ Apply these rules in order. First match wins.
**Triggers**: User explicitly asks about "API endpoints", "request format", "response schema", "API reference", or mentions inspecting HTTP details.
**Action**: Read `references/workos-api-[feature].md` where `[feature]` matches the domain (admin-portal, audit-logs, authkit, directory-sync, events, organization, roles, sso, vault, widgets).
**Action**: Read `references/workos-api-[feature].md` where `[feature]` matches the domain (admin-portal, audit-logs, authkit, directory-sync, events, organization, roles, sso, vault).
**Why this wins**: API references are low-level; feature guides are high-level. If user signals low-level intent, skip the feature guide.
@@ -103,9 +102,11 @@ Apply these rules in order. First match wins.
### 3. Feature-Specific Request
**Triggers**: User mentions a specific WorkOS feature by name (SSO, MFA, Directory Sync, Audit Logs, Vault, RBAC, Admin Portal, Custom Domains, Widgets, Events, Integrations, Email).
**Triggers**: User mentions a specific WorkOS feature by name (SSO, MFA, Directory Sync, Audit Logs, Vault, RBAC, Admin Portal, Custom Domains, Events, Integrations, Email).
**Action**: Read `references/workos-[feature].md` where `[feature]` is the lowercase slug (sso, mfa, directory-sync, audit-logs, vault, rbac, admin-portal, custom-domains, widgets, events, integrations, email).
**Action**: Read `references/workos-[feature].md` where `[feature]` is the lowercase slug (sso, mfa, directory-sync, audit-logs, vault, rbac, admin-portal, custom-domains, events, integrations, email).
**Exception**: Widget requests route to the `workos-widgets` skill via the Skill tool (see table above), not to a `references/` file.
**Disambiguation**: If user mentions BOTH a feature and "API", route to API reference (#2). If they mention MULTIPLE features, route to the MOST SPECIFIC one first (e.g., "SSO with MFA" → route to SSO; user can request MFA separately).
@@ -1,23 +0,0 @@
<!-- generated:sha256:eda510c1c51f -->
# WorkOS Widgets API Reference — Quick Reference
## Step 1: Fetch Documentation
**WebFetch the API reference before making calls.**
- https://workos.com/docs/reference/widgets
- https://workos.com/docs/reference/widgets/get-token
## Endpoints
| Endpoint | Description |
| ------------ | ------------------- |
| `/widgets` | widgets |
| `/get-token` | widgets - get-token |
## Implementation
For integration patterns, error recovery, and verification:
> Read `references/workos-widgets.guide.md`
@@ -1,19 +0,0 @@
<!-- refined:sha256:eda510c1c51f -->
# WorkOS Widgets API Reference
## When to Use
Use this skill when you need to generate secure, short-lived tokens for embedded WorkOS UI components (widgets). The Widgets API provides a single endpoint (`/get-token`) that creates tokens scoped to specific widget types and contexts, allowing you to embed WorkOS functionality directly in your application's interface without building custom UI.
## Key Vocabulary
- **Widget Token** — short-lived JWT that authorizes a specific widget instance
- **Widget Type** — identifier for the embedded component (e.g., `user_management`, `organization_switcher`)
- **Scope Context** — parameters that limit what the widget can access (organization ID, user ID, etc.)
## Implementation Guide
For step-by-step implementation, verification commands, and error recovery:
→ Read `references/workos-api-widgets.guide.md`
@@ -1,207 +0,0 @@
<!-- refined:sha256:883decb5b1de -->
# WorkOS Widgets — Implementation Guide
## Step 1: Fetch Documentation (BLOCKING)
**STOP. Do not proceed until complete.**
WebFetch these docs for latest widget integration details:
- https://workos.com/docs/widgets/user-sessions
- https://workos.com/docs/widgets/user-security
- https://workos.com/docs/widgets/user-profile
- https://workos.com/docs/widgets/user-management
- https://workos.com/docs/widgets/tokens
- https://workos.com/docs/widgets/quick-start
- https://workos.com/docs/widgets/pipes
- https://workos.com/docs/widgets/organization-switcher
The docs are the source of truth. If this skill conflicts with docs, follow docs.
## Step 2: Pre-Flight Validation
### Environment Variables
Check `.env` or `.env.local` for:
- `WORKOS_API_KEY` - starts with `sk_`
- `WORKOS_CLIENT_ID` - starts with `client_`
**Verify before continuing:**
```bash
echo $WORKOS_API_KEY | grep '^sk_' && echo "✓ valid API key" || echo "✗ invalid/missing"
echo $WORKOS_CLIENT_ID | grep '^client_' && echo "✓ valid client ID" || echo "✗ invalid/missing"
```
### SDK Presence
Confirm WorkOS SDK is installed:
```bash
npm list @workos-inc/node 2>/dev/null || echo "FAIL: SDK not installed"
```
## Step 3: Token Generation (CRITICAL)
**All widgets require a secure token.** This token must be generated server-side — NEVER expose API keys to the client.
Create a dedicated API endpoint:
```javascript
// Token generation pattern (server-side only)
const token = await workos.widgets.generateToken({
userId: currentUser.id,
organizationId: currentUser.organizationId, // Required for org-scoped widgets
scopes: ['widgets:user-profile'], // Widget-specific scopes
});
return { token };
```
**Decision tree for scope selection:**
```
Which widget?
|
+-- UserProfile --> scopes: ['widgets:user-profile']
|
+-- UserSecurity --> scopes: ['widgets:user-security']
|
+-- UserSessions --> scopes: ['widgets:user-sessions']
|
+-- UserManagement --> scopes: ['widgets:user-management']
|
+-- OrganizationSwitcher --> scopes: ['widgets:organization-switcher']
|
+-- Multiple widgets --> scopes: [array of all needed scopes]
```
Check fetched docs for complete scope names and token expiry behavior.
## Step 4: Client-Side Integration
### Basic Widget Mounting
Fetch token from your API endpoint, then mount widget:
```javascript
// Fetch token from your server
const response = await fetch('/api/workos-widget-token');
const { token } = await response.json();
// Mount widget with token
workos.widgets.mount({
element: '#widget-container', // DOM selector
token: token,
widget: 'user-profile', // Widget type
});
```
### Widget Type Names (Decision Tree)
```
Widget display goal?
|
+-- View/edit profile --> widget: 'user-profile'
|
+-- Manage password/MFA --> widget: 'user-security'
|
+-- View active sessions --> widget: 'user-sessions'
|
+-- Admin user list --> widget: 'user-management'
|
+-- Switch organizations --> widget: 'organization-switcher'
```
Check fetched docs for exact widget type strings — they may differ by SDK version.
## Step 5: Pipes Configuration (Advanced)
**Pipes** allow widgets to communicate with your app (e.g., trigger actions when user updates profile).
Define pipe handlers when mounting:
```javascript
workos.widgets.mount({
element: '#widget-container',
token: token,
widget: 'user-profile',
pipes: {
onProfileUpdate: (data) => {
// Handle profile change in your app
refreshUserData(data.userId);
},
},
});
```
Check fetched docs for:
- Available pipe names per widget
- Pipe payload schemas
- Error handling patterns
## Verification Checklist (ALL MUST PASS)
```bash
# 1. Token endpoint exists and returns valid token
curl -s http://localhost:3000/api/workos-widget-token | jq -e '.token' || echo "FAIL: No token endpoint"
# 2. Client code imports widget SDK
grep -r "workos\.widgets" src/ || echo "FAIL: No widget mounting code"
# 3. No API key exposed in client code
grep -r "WORKOS_API_KEY" src/pages src/components 2>/dev/null && echo "FAIL: API key leaked to client" || echo "✓ No key leakage"
# 4. Build succeeds
npm run build
```
## Error Recovery
### "Invalid token" or 401 Unauthorized
**Root cause:** Token expired, missing scopes, or wrong userId.
Fix:
1. Check token generation includes correct `userId` matching the authenticated user
2. Verify scopes array matches widget type (see Step 3 decision tree)
3. Check token hasn't expired — regenerate fresh token for each widget mount
### Widget fails to render / blank container
**Root cause:** Element selector doesn't match DOM, or SDK not loaded.
Fix:
1. Verify element exists before calling `mount()`: `document.querySelector('#widget-container')`
2. Check SDK loaded: `typeof workos !== 'undefined'`
3. Inspect browser console for CORS or network errors
### "organizationId required" error
**Root cause:** Widget needs org context but token generated without it.
Fix:
1. Add `organizationId` to token generation call
2. Ensure user is member of the organization
3. For UserManagement widget, organizationId is MANDATORY
### Widget renders but actions fail (save, delete, etc.)
**Root cause:** Insufficient token scopes.
Fix:
1. Check token scopes match widget requirements exactly (see Step 3)
2. Regenerate token with correct scopes
3. Verify API key has permissions for widget operations in WorkOS Dashboard
## Related Skills
- workos-authkit-react
- workos-authkit-nextjs
- workos-authkit-vanilla-js
@@ -1,23 +0,0 @@
<!-- refined:sha256:883decb5b1de -->
# WorkOS Widgets
## When to Use
Use this skill when you need to generate secure, short-lived tokens for embedding WorkOS UI components (Admin Portal, Log Streams) directly into your application. Widgets tokens provide scoped access to specific WorkOS resources without requiring full API key management in the frontend.
## Key Vocabulary
- **Widget Token** — short-lived JWT (15-60 minutes) authorizing access to a specific WorkOS UI component
- **Organization `org_`** — the entity whose resources the widget will display (Admin Portal, Log Streams)
- **User `user_`** — optional identifier for tracking which user accessed the widget
## Implementation Guide
For step-by-step implementation, verification commands, and error recovery:
→ Read `references/workos-widgets.guide.md`
## Related Skills
- **workos-admin-portal**: Admin Portal for enterprise management
+3
View File
@@ -15,6 +15,9 @@ importers:
'@types/node':
specifier: ^22.0.0
version: 22.19.11
esbuild:
specifier: ^0.27.3
version: 0.27.3
oxfmt:
specifier: ^0.35.0
version: 0.35.0
+2 -1
View File
@@ -36,6 +36,7 @@ export const HAND_CRAFTED_SKILLS = [
'workos-authkit-react-router',
'workos-authkit-tanstack-start',
'workos-authkit-vanilla-js',
'workos-widgets',
] as const;
/** Hand-crafted guide files that must never be overwritten by generation */
@@ -47,7 +48,7 @@ export const SECTION_CONFIG: Record<string, SectionConfig> = {
'on-prem-deployment': { split: { strategy: 'single' }, skip: true },
glossary: { split: { strategy: 'single' }, skip: true },
email: { split: { strategy: 'single' } },
widgets: { split: { strategy: 'single' } },
widgets: { split: { strategy: 'single' }, skip: true },
vault: { split: { strategy: 'single' } },
sso: { split: { strategy: 'single' } },
sdks: { split: { strategy: 'single' }, skip: true },
+1
View File
@@ -84,6 +84,7 @@ describe('path resolution', () => {
'workos-authkit-react-router',
'workos-authkit-tanstack-start',
'workos-authkit-vanilla-js',
'workos-widgets',
]);
// Skills referenced in Related Skills but not generated (skipped sections)
const KNOWN_MISSING = new Set([
+457
View File
@@ -0,0 +1,457 @@
import { describe, expect, it } from 'vitest';
import {
resolveRef,
resolveSchema,
extractEndpoints,
formatEndpoint,
groupPathsByWidget,
loadSpec,
WIDGET_PREFIXES,
type EndpointInfo,
} from '../../plugins/workos/skills/workos-widgets/references/scripts/query-spec.ts';
// --- Minimal spec fixtures ---
function makeSpec(overrides: Record<string, unknown> = {}) {
return {
paths: {
'/_widgets/UserProfile/me': {
get: {
description: 'Returns the current user profile',
parameters: [],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Me' },
},
},
},
'403': {
description: 'Forbidden',
content: {
'application/json': {
schema: {
type: 'object',
properties: { message: { type: 'string' } },
required: ['message'],
},
},
},
},
},
},
post: {
description: 'Updates the current user profile',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
firstName: { type: 'string' },
lastName: { type: 'string' },
},
},
},
},
},
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Me' },
},
},
},
},
},
},
'/_widgets/UserManagement/members': {
get: {
description: 'List members',
parameters: [
{ name: 'limit', in: 'query', schema: { type: 'integer' } },
{ name: 'after', in: 'query', schema: { type: 'string' } },
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/MembersList' },
},
},
},
},
},
},
'/_widgets/admin-portal/sso-connections': {
get: {
description: 'List SSO connections',
responses: {
'200': { description: 'OK' },
},
},
},
'/_widgets/settings': {
get: {
description: 'Get widget settings',
responses: {
'200': { description: 'OK' },
},
},
},
},
components: {
schemas: {
Me: {
type: 'object',
properties: {
id: { type: 'string' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
},
required: ['id', 'email'],
},
MembersList: {
type: 'object',
properties: {
data: {
type: 'array',
items: { $ref: '#/components/schemas/Member' },
},
},
},
Member: {
type: 'object',
properties: {
userId: { type: 'string' },
role: { type: 'string' },
},
},
},
},
...overrides,
} as Record<string, unknown>;
}
// --- resolveRef ---
describe('resolveRef', () => {
const spec = makeSpec();
it('resolves a top-level schema $ref', () => {
const result = resolveRef(spec, '#/components/schemas/Me') as Record<string, unknown>;
expect(result).toBeDefined();
expect(result.type).toBe('object');
expect(result.required).toEqual(['id', 'email']);
});
it('returns undefined for non-existent ref', () => {
expect(resolveRef(spec, '#/components/schemas/DoesNotExist')).toBeUndefined();
});
it('returns undefined for deeply broken path', () => {
expect(resolveRef(spec, '#/a/b/c/d/e')).toBeUndefined();
});
it('resolves nested refs', () => {
const result = resolveRef(spec, '#/components/schemas/MembersList') as Record<string, unknown>;
expect(result).toBeDefined();
const props = result.properties as Record<string, unknown>;
const data = props.data as Record<string, unknown>;
// The items $ref is NOT resolved by resolveRef — it just navigates the path
expect(data.items).toEqual({ $ref: '#/components/schemas/Member' });
});
});
// --- resolveSchema ---
describe('resolveSchema', () => {
const spec = makeSpec();
it('resolves a $ref schema to its definition', () => {
const result = resolveSchema(spec, { $ref: '#/components/schemas/Me' }) as Record<string, unknown>;
expect(result.type).toBe('object');
expect((result.properties as Record<string, unknown>).id).toEqual({ type: 'string' });
});
it('recursively resolves nested $refs', () => {
const result = resolveSchema(spec, { $ref: '#/components/schemas/MembersList' }) as Record<string, unknown>;
const props = result.properties as Record<string, unknown>;
const data = props.data as Record<string, unknown>;
// items.$ref should be resolved to the actual Member schema
const items = data.items as Record<string, unknown>;
expect(items.type).toBe('object');
expect((items.properties as Record<string, unknown>).userId).toEqual({ type: 'string' });
});
it('returns primitives unchanged', () => {
expect(resolveSchema(spec, 'hello')).toBe('hello');
expect(resolveSchema(spec, 42)).toBe(42);
expect(resolveSchema(spec, null)).toBeNull();
expect(resolveSchema(spec, undefined)).toBeUndefined();
});
it('preserves arrays and resolves items within them', () => {
const schema = { oneOf: [{ $ref: '#/components/schemas/Me' }, { type: 'null' }] };
const result = resolveSchema(spec, schema) as Record<string, unknown>;
const oneOf = result.oneOf as unknown[];
expect(oneOf).toHaveLength(2);
// First item should be the resolved Me schema
expect((oneOf[0] as Record<string, unknown>).type).toBe('object');
// Second item stays as-is
expect((oneOf[1] as Record<string, unknown>).type).toBe('null');
});
it('handles schema with no $ref (pass-through)', () => {
const schema = { type: 'object', properties: { name: { type: 'string' } } };
const result = resolveSchema(spec, schema) as Record<string, unknown>;
expect(result.type).toBe('object');
});
it('returns undefined for unresolvable $ref', () => {
const result = resolveSchema(spec, { $ref: '#/components/schemas/Missing' });
expect(result).toBeUndefined();
});
});
// --- extractEndpoints ---
describe('extractEndpoints', () => {
const spec = makeSpec();
it('extracts endpoints matching a path prefix', () => {
const endpoints = extractEndpoints(spec, (p) => p.startsWith('/_widgets/UserProfile'));
expect(endpoints).toHaveLength(2);
expect(endpoints.map((e) => e.method).sort()).toEqual(['GET', 'POST']);
expect(endpoints.every((e) => e.path === '/_widgets/UserProfile/me')).toBe(true);
});
it('extracts a single endpoint by exact path', () => {
const endpoints = extractEndpoints(spec, (p) => p === '/_widgets/UserManagement/members');
expect(endpoints).toHaveLength(1);
expect(endpoints[0].method).toBe('GET');
expect(endpoints[0].description).toBe('List members');
});
it('returns empty array when no paths match', () => {
const endpoints = extractEndpoints(spec, () => false);
expect(endpoints).toEqual([]);
});
it('returns empty array for spec with no paths', () => {
const endpoints = extractEndpoints({ paths: undefined } as unknown as Record<string, unknown>, () => true);
expect(endpoints).toEqual([]);
});
it('resolves $ref schemas in responses', () => {
const endpoints = extractEndpoints(spec, (p) => p === '/_widgets/UserProfile/me');
const get = endpoints.find((e) => e.method === 'GET')!;
const resp200 = get.responses['200'] as Record<string, unknown>;
const schema = resp200.schema as Record<string, unknown>;
// Should be the resolved Me schema, not a $ref
expect(schema.type).toBe('object');
expect(schema.required).toEqual(['id', 'email']);
});
it('resolves $ref schemas in request bodies', () => {
const endpoints = extractEndpoints(spec, (p) => p === '/_widgets/UserProfile/me');
const post = endpoints.find((e) => e.method === 'POST')!;
expect(post.requestBody).toBeDefined();
const body = post.requestBody as Record<string, unknown>;
expect(body.type).toBe('object');
expect((body.properties as Record<string, unknown>).firstName).toEqual({ type: 'string' });
});
it('includes parameters when present', () => {
const endpoints = extractEndpoints(spec, (p) => p === '/_widgets/UserManagement/members');
expect(endpoints[0].parameters).toHaveLength(2);
});
it('handles responses without content/schema', () => {
const endpoints = extractEndpoints(spec, (p) => p === '/_widgets/admin-portal/sso-connections');
const get = endpoints[0];
const resp200 = get.responses['200'] as Record<string, unknown>;
expect(resp200.description).toBe('OK');
expect(resp200.schema).toBeUndefined();
});
it('includes inline error response schemas', () => {
const endpoints = extractEndpoints(spec, (p) => p === '/_widgets/UserProfile/me');
const get = endpoints.find((e) => e.method === 'GET')!;
const resp403 = get.responses['403'] as Record<string, unknown>;
const schema = resp403.schema as Record<string, unknown>;
expect(schema.type).toBe('object');
expect((schema.properties as Record<string, unknown>).message).toEqual({ type: 'string' });
});
});
// --- formatEndpoint ---
describe('formatEndpoint', () => {
it('formats a basic GET endpoint', () => {
const ep: EndpointInfo = {
path: '/_widgets/UserProfile/me',
method: 'GET',
description: 'Returns the current user profile',
responses: {
'200': { description: 'OK', schema: { type: 'object' } },
},
};
const output = formatEndpoint(ep);
expect(output).toContain('## GET /_widgets/UserProfile/me');
expect(output).toContain('Returns the current user profile');
expect(output).toContain('### Response 200 — OK');
expect(output).toContain('```json');
});
it('formats request body', () => {
const ep: EndpointInfo = {
path: '/_widgets/UserProfile/me',
method: 'POST',
requestBody: { type: 'object', properties: { firstName: { type: 'string' } } },
responses: {},
};
const output = formatEndpoint(ep);
expect(output).toContain('### Request Body');
expect(output).toContain('"firstName"');
});
it('formats parameters', () => {
const ep: EndpointInfo = {
path: '/_widgets/UserManagement/members',
method: 'GET',
parameters: [{ name: 'limit', in: 'query', schema: { type: 'integer' } }],
responses: {},
};
const output = formatEndpoint(ep);
expect(output).toContain('### Parameters');
expect(output).toContain('`limit` (query)');
});
it('omits description when not present', () => {
const ep: EndpointInfo = {
path: '/_widgets/settings',
method: 'GET',
responses: { '200': { description: 'OK' } },
};
const output = formatEndpoint(ep);
const lines = output.split('\n');
// Second line should be empty or the response, not a description
expect(lines[0]).toBe('## GET /_widgets/settings');
});
it('omits parameters section when empty', () => {
const ep: EndpointInfo = {
path: '/_widgets/settings',
method: 'GET',
parameters: [],
responses: {},
};
const output = formatEndpoint(ep);
expect(output).not.toContain('### Parameters');
});
it('handles response without schema', () => {
const ep: EndpointInfo = {
path: '/_widgets/settings',
method: 'GET',
responses: { '200': { description: 'OK' } },
};
const output = formatEndpoint(ep);
expect(output).toContain('### Response 200 — OK');
expect(output).not.toContain('```json');
});
});
// --- groupPathsByWidget ---
describe('groupPathsByWidget', () => {
it('groups paths by their first two segments', () => {
const paths = [
'/_widgets/UserProfile/me',
'/_widgets/UserProfile/sessions',
'/_widgets/UserManagement/members',
'/_widgets/settings',
];
const grouped = groupPathsByWidget(paths);
expect(grouped['_widgets/UserProfile']).toHaveLength(2);
expect(grouped['_widgets/UserManagement']).toHaveLength(1);
expect(grouped['_widgets/settings']).toHaveLength(1);
});
it('returns empty object for empty input', () => {
expect(groupPathsByWidget([])).toEqual({});
});
});
// --- WIDGET_PREFIXES ---
describe('WIDGET_PREFIXES', () => {
it('has entries for all core widgets', () => {
expect(WIDGET_PREFIXES['usermanagement']).toBe('/_widgets/UserManagement');
expect(WIDGET_PREFIXES['userprofile']).toBe('/_widgets/UserProfile');
expect(WIDGET_PREFIXES['admin-portal']).toBe('/_widgets/admin-portal');
expect(WIDGET_PREFIXES['adminportal']).toBe('/_widgets/admin-portal');
});
it('has alias entries for sub-widgets', () => {
expect(WIDGET_PREFIXES['sso-connection']).toContain('sso-connections');
expect(WIDGET_PREFIXES['domain-verification']).toContain('organization-domains');
});
it('all prefixes start with /_widgets/', () => {
for (const prefix of Object.values(WIDGET_PREFIXES)) {
expect(prefix).toMatch(/^\/_widgets\//);
}
});
});
// --- Integration: loadSpec + extractEndpoints against real spec ---
describe('integration with real spec', () => {
const spec = loadSpec();
it('loads the real spec with paths', () => {
expect(spec.paths).toBeDefined();
expect(Object.keys(spec.paths).length).toBeGreaterThan(30);
});
it('all WIDGET_PREFIXES match at least one path in the real spec', () => {
const allPaths = Object.keys(spec.paths);
const uniquePrefixes = new Set(Object.values(WIDGET_PREFIXES));
for (const prefix of uniquePrefixes) {
const matches = allPaths.filter((p) => p.startsWith(prefix));
expect(matches.length, `prefix "${prefix}" should match at least one path`).toBeGreaterThan(0);
}
});
it('UserProfile filter returns 14+ endpoints from real spec', () => {
const endpoints = extractEndpoints(spec, (p) => p.startsWith('/_widgets/UserProfile'));
expect(endpoints.length).toBeGreaterThanOrEqual(14);
});
it('UserManagement filter returns 8+ endpoints from real spec', () => {
const endpoints = extractEndpoints(spec, (p) => p.startsWith('/_widgets/UserManagement'));
expect(endpoints.length).toBeGreaterThanOrEqual(8);
});
it('admin-portal filter returns 4+ endpoints from real spec', () => {
const endpoints = extractEndpoints(spec, (p) => p.startsWith('/_widgets/admin-portal'));
expect(endpoints.length).toBeGreaterThanOrEqual(4);
});
it('resolved schemas have no remaining $ref keys', () => {
const endpoints = extractEndpoints(spec, (p) => p.startsWith('/_widgets/UserProfile/me'));
const get = endpoints.find((e) => e.method === 'GET')!;
const resp200 = get.responses['200'] as Record<string, unknown>;
const json = JSON.stringify(resp200.schema);
expect(json).not.toContain('$ref');
});
});
+2 -2
View File
@@ -174,9 +174,9 @@ describe('splitSections', () => {
});
it('attaches doc URLs from llms.txt', () => {
const sections = [makeSection('widgets')];
const sections = [makeSection('vault')];
const urls = new Map([
['widgets', ['https://workos.com/docs/widgets/quick-start', 'https://workos.com/docs/widgets/user-profile']],
['vault', ['https://workos.com/docs/vault/quick-start', 'https://workos.com/docs/vault/encrypt']],
]);
const specs = splitSections(sections, urls);
expect(specs[0].docUrls).toHaveLength(2);