feat: scaffold @workos-inc/skills package with parser, fetcher, and 6 AuthKit skills

Phase 1 of the WorkOS skills generator. Sets up the repo as a publishable
npm package compatible with skills.sh, copies 6 hand-crafted AuthKit
framework skills from the CLI repo, and builds the foundation for skill
generation from llms-full.txt.

- npm package: @workos-inc/skills with skills/ in files array
- Fetcher: downloads llms.txt/llms-full.txt with retry + local cache
- Parser: extracts 24-section tree from ## Name {#anchor} boundaries
- Validator: format guards that fail loudly on doc structure changes
- 18 passing tests (bun test)
This commit is contained in:
Nick Nisi
2026-02-06 11:36:11 -05:00
commit 718badf413
20 changed files with 1793 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.cache/
dist/
*.tsbuildinfo
scripts/output/
+6
View File
@@ -0,0 +1,6 @@
node_modules
.cache
dist
bun.lockb
scripts/output
skills/*/SKILL.md
+27
View File
@@ -0,0 +1,27 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@workos-inc/skills",
"devDependencies": {
"@types/bun": "latest",
"prettier": "^3.8.1",
"typescript": "^5.7.0",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/node": ["@types/node@25.2.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg=="],
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@workos-inc/skills",
"version": "0.1.0",
"type": "module",
"description": "WorkOS Skills for AI coding agents — AuthKit, SSO, Directory Sync, RBAC, and more",
"files": [
"skills"
],
"scripts": {
"generate": "bun run scripts/generate.ts",
"test": "bun test",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"keywords": [
"workos",
"skills",
"authkit",
"sso",
"claude-code",
"ai-agents"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/workos/skills"
},
"devDependencies": {
"@types/bun": "latest",
"prettier": "^3.8.1",
"typescript": "^5.7.0"
}
}
+47
View File
@@ -0,0 +1,47 @@
import { fetchLlmsFullTxt } from "./lib/fetcher.ts";
import { parseSections } from "./lib/parser.ts";
import { validateSections } from "./lib/validator.ts";
async function main() {
console.log("Fetching llms-full.txt...");
const { content, source } = await fetchLlmsFullTxt();
console.log(` Source: ${source}, ${(content.length / 1024).toFixed(0)}KB`);
console.log("\nParsing sections...");
const sections = parseSections(content);
console.log(` Found ${sections.length} sections`);
for (const section of sections) {
const subs = section.subsections.length;
console.log(
` ${section.anchor.padEnd(20)} ${(section.sizeBytes / 1024).toFixed(0).padStart(5)}KB ${subs} subsections`,
);
}
console.log("\nValidating...");
const result = validateSections(sections);
if (result.warnings.length > 0) {
console.log("\nWarnings:");
for (const w of result.warnings) {
console.log(`${w}`);
}
}
if (!result.valid) {
console.error("\nValidation FAILED:");
for (const e of result.errors) {
console.error(`${e}`);
}
process.exit(1);
}
console.log(
`\n✓ Validation passed. ${result.sectionCount} sections, ${(result.totalSize / 1024).toFixed(0)}KB total`,
);
}
main().catch((err) => {
console.error("Fatal:", err.message);
process.exit(1);
});
+78
View File
@@ -0,0 +1,78 @@
import type { SectionConfig } from "./types.ts";
/** Known section anchors from llms-full.txt as of 2026-02-06 */
export const KNOWN_ANCHORS = [
"postman",
"on-prem-deployment",
"glossary",
"email",
"widgets",
"vault",
"sso",
"sdks",
"reference",
"rbac",
"pipes",
"migrate",
"mfa",
"magic-link",
"integrations",
"fga",
"feature-flags",
"events",
"domain-verification",
"directory-sync",
"custom-domains",
"authkit",
"audit-logs",
"admin-portal",
] as const;
/** Hand-crafted skill names that must never be overwritten */
export const HAND_CRAFTED_SKILLS = [
"workos-authkit-base",
"workos-authkit-nextjs",
"workos-authkit-react",
"workos-authkit-react-router",
"workos-authkit-tanstack-start",
"workos-authkit-vanilla-js",
] as const;
/** Per-section split strategy configuration */
export const SECTION_CONFIG: Record<string, SectionConfig> = {
postman: { split: { strategy: "single" }, skip: true },
"on-prem-deployment": { split: { strategy: "single" }, skip: true },
glossary: { split: { strategy: "single" }, skip: true },
email: { split: { strategy: "single" } },
widgets: { split: { strategy: "single" } },
vault: { split: { strategy: "single" } },
sso: { split: { strategy: "single" } },
sdks: { split: { strategy: "single" }, skip: true },
reference: { split: { strategy: "per-api-domain" } },
rbac: { split: { strategy: "single" } },
pipes: { split: { strategy: "single" } },
migrate: { split: { strategy: "per-subsection" } },
mfa: { split: { strategy: "single" } },
"magic-link": { split: { strategy: "single" } },
integrations: { split: { strategy: "single" } },
fga: { split: { strategy: "single" } },
"feature-flags": { split: { strategy: "single" } },
events: { split: { strategy: "single" } },
"domain-verification": { split: { strategy: "single" } },
"directory-sync": { split: { strategy: "single" } },
"custom-domains": { split: { strategy: "single" } },
authkit: { split: { strategy: "skip" } },
"audit-logs": { split: { strategy: "single" } },
"admin-portal": { split: { strategy: "single" } },
};
/** Validation thresholds */
export const VALIDATION = {
expectedSectionCount: 24,
sectionCountTolerance: 3,
maxSectionSize: 600_000,
minTotalSize: 700_000,
maxTotalSize: 1_800_000,
minSkillSize: 500,
maxSkillSize: 50_000,
} as const;
+116
View File
@@ -0,0 +1,116 @@
import { join } from "path";
import { mkdir } from "fs/promises";
import type { FetchResult, FetchOptions } from "./types.ts";
const LLMS_TXT_URL = "https://workos.com/docs/llms.txt";
const LLMS_FULL_TXT_URL = "https://workos.com/docs/llms-full.txt";
const DEFAULT_CACHE_DIR = ".cache";
const DEFAULT_MAX_AGE = 60 * 60 * 1000; // 1 hour
const DEFAULT_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
function getCacheFilePath(cacheDir: string, url: string): string {
const filename = url.split("/").pop() ?? "unknown";
return join(cacheDir, filename);
}
function getMetaFilePath(cachePath: string): string {
return `${cachePath}.meta.json`;
}
async function readCache(
cachePath: string,
maxAge: number,
): Promise<FetchResult | null> {
const metaPath = getMetaFilePath(cachePath);
try {
const meta = JSON.parse(await Bun.file(metaPath).text());
const age = Date.now() - new Date(meta.fetchedAt).getTime();
if (age > maxAge) return null;
const content = await Bun.file(cachePath).text();
return {
content,
source: "cache",
fetchedAt: new Date(meta.fetchedAt),
};
} catch {
return null;
}
}
async function writeCache(
cachePath: string,
content: string,
fetchedAt: Date,
): Promise<void> {
try {
await mkdir(cachePath.split("/").slice(0, -1).join("/"), {
recursive: true,
});
await Bun.write(cachePath, content);
await Bun.write(
getMetaFilePath(cachePath),
JSON.stringify({ fetchedAt: fetchedAt.toISOString() }),
);
} catch (err) {
console.warn(`Warning: Could not write cache to ${cachePath}:`, err);
}
}
async function fetchWithRetry(url: string, retries: number): Promise<string> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, { redirect: "follow" });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.text();
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
if (attempt < retries) {
await Bun.sleep(RETRY_DELAY_MS * attempt);
}
}
}
throw new Error(
`Failed to fetch ${url} after ${retries} retries. Last error: ${lastError?.message}`,
);
}
export async function fetchDocs(
url: string,
opts?: FetchOptions,
): Promise<FetchResult> {
const cacheDir = opts?.cacheDir ?? DEFAULT_CACHE_DIR;
const maxAge = opts?.maxAge ?? DEFAULT_MAX_AGE;
const retries = opts?.retries ?? DEFAULT_RETRIES;
const cachePath = getCacheFilePath(cacheDir, url);
const cached = await readCache(cachePath, maxAge);
if (cached) return cached;
const content = await fetchWithRetry(url, retries);
const fetchedAt = new Date();
await writeCache(cachePath, content, fetchedAt);
return { content, source: "network", fetchedAt };
}
export async function fetchLlmsTxt(opts?: FetchOptions): Promise<FetchResult> {
return fetchDocs(LLMS_TXT_URL, opts);
}
export async function fetchLlmsFullTxt(
opts?: FetchOptions,
): Promise<FetchResult> {
return fetchDocs(LLMS_FULL_TXT_URL, opts);
}
+128
View File
@@ -0,0 +1,128 @@
import type { Section, Subsection } from "./types.ts";
/**
* Find positions of all fenced code blocks (``` ... ```) so we can
* skip any headers that appear inside them.
*/
function findCodeBlockRanges(
content: string,
): Array<{ start: number; end: number }> {
const ranges: Array<{ start: number; end: number }> = [];
const fenceRe = /^```/gm;
let match: RegExpExecArray | null;
let openStart: number | null = null;
while ((match = fenceRe.exec(content)) !== null) {
if (openStart === null) {
openStart = match.index;
} else {
ranges.push({ start: openStart, end: match.index + match[0].length });
openStart = null;
}
}
return ranges;
}
function isInsideCodeBlock(
position: number,
ranges: Array<{ start: number; end: number }>,
): boolean {
return ranges.some((r) => position >= r.start && position <= r.end);
}
/**
* Parse llms-full.txt into a section tree.
* Splits on ## SectionName {#anchor} boundaries.
*/
export function parseSections(markdown: string): Section[] {
const codeBlockRanges = findCodeBlockRanges(markdown);
const sections: Section[] = [];
// Find all section header positions
const headers: Array<{
name: string;
anchor: string;
index: number;
fullMatchEnd: number;
}> = [];
const re = /^## (.+?) \{#([a-z0-9-]+)\}\s*$/gm;
let match: RegExpExecArray | null;
while ((match = re.exec(markdown)) !== null) {
if (!isInsideCodeBlock(match.index, codeBlockRanges)) {
headers.push({
name: match[1],
anchor: match[2],
index: match.index,
fullMatchEnd: match.index + match[0].length,
});
}
}
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const start = header.fullMatchEnd;
const end = i + 1 < headers.length ? headers[i + 1].index : markdown.length;
const content = markdown.slice(start, end).trim();
const subsections = parseSubsections(content);
sections.push({
name: header.name,
anchor: header.anchor,
content,
sizeBytes: Buffer.byteLength(content, "utf8"),
lineCount: content.split("\n").length,
subsections,
});
}
return sections;
}
/**
* Parse subsections within a section's content.
* Splits on ### Heading boundaries, skipping headings inside code blocks.
*/
export function parseSubsections(sectionContent: string): Subsection[] {
const codeBlockRanges = findCodeBlockRanges(sectionContent);
const subsections: Subsection[] = [];
const headers: Array<{
title: string;
index: number;
fullMatchEnd: number;
}> = [];
const re = /^### (.+)$/gm;
let match: RegExpExecArray | null;
while ((match = re.exec(sectionContent)) !== null) {
if (!isInsideCodeBlock(match.index, codeBlockRanges)) {
headers.push({
title: match[1],
index: match.index,
fullMatchEnd: match.index + match[0].length,
});
}
}
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const start = header.fullMatchEnd;
const end =
i + 1 < headers.length ? headers[i + 1].index : sectionContent.length;
const content = sectionContent.slice(start, end).trim();
subsections.push({
title: header.title,
level: 3,
content,
sizeBytes: Buffer.byteLength(content, "utf8"),
});
}
return subsections;
}
+91
View File
@@ -0,0 +1,91 @@
/** A top-level section from llms-full.txt, delimited by ## Name {#anchor} */
export interface Section {
/** Display name, e.g. "Single Sign-On" */
name: string;
/** Anchor slug, e.g. "sso" */
anchor: string;
/** Full markdown content between this header and the next */
content: string;
sizeBytes: number;
lineCount: number;
subsections: Subsection[];
}
/** A subsection within a top-level section, delimited by ### Heading */
export interface Subsection {
title: string;
/** Heading level (2 or 3) */
level: number;
content: string;
sizeBytes: number;
}
/** Specification for a skill to be generated */
export interface SkillSpec {
/** Skill directory name, e.g. "workos-sso" */
name: string;
/** Action-oriented 1-liner for frontmatter */
description: string;
/** Display title, e.g. "WorkOS Single Sign-On" */
title: string;
/** Source section anchor */
anchor: string;
/** Raw section content (context for generation) */
content: string;
/** Doc page URLs from llms.txt for runtime WebFetch */
docUrls: string[];
/** true = generated by script, false = hand-crafted */
generated: boolean;
}
/** A generated skill ready to write to disk */
export interface GeneratedSkill {
name: string;
/** Relative path, e.g. "skills/workos-sso/SKILL.md" */
path: string;
content: string;
sizeBytes: number;
generated: boolean;
}
// --- Split strategies ---
export type SplitStrategy =
| { strategy: "single" }
| { strategy: "per-subsection"; groupChildren?: boolean }
| { strategy: "per-feature"; features: string[] }
| { strategy: "per-api-domain" }
| { strategy: "skip" };
export interface SectionConfig {
split: SplitStrategy;
/** If true, don't generate a skill for this section */
skip?: boolean;
}
// --- Fetcher ---
export interface FetchResult {
content: string;
source: "cache" | "network";
fetchedAt: Date;
}
export interface FetchOptions {
/** Cache directory path. Default: .cache/ */
cacheDir?: string;
/** Cache TTL in ms. Default: 1 hour */
maxAge?: number;
/** Max retry attempts. Default: 3 */
retries?: number;
}
// --- Validator ---
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
sectionCount: number;
totalSize: number;
}
+76
View File
@@ -0,0 +1,76 @@
import type { Section, ValidationResult } from "./types.ts";
import { KNOWN_ANCHORS, VALIDATION } from "./config.ts";
/**
* Validate parsed sections against expected structure.
* Fails loudly with actionable error messages when llms-full.txt format changes.
*/
export function validateSections(sections: Section[]): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
const totalSize = sections.reduce((sum, s) => sum + s.sizeBytes, 0);
// Section count check
const countDiff = Math.abs(sections.length - VALIDATION.expectedSectionCount);
if (countDiff > VALIDATION.sectionCountTolerance) {
errors.push(
`Expected ~${VALIDATION.expectedSectionCount} sections (±${VALIDATION.sectionCountTolerance}), got ${sections.length}`,
);
} else if (sections.length !== VALIDATION.expectedSectionCount) {
warnings.push(
`Section count changed: expected ${VALIDATION.expectedSectionCount}, got ${sections.length}`,
);
}
// Known anchors present
const foundAnchors = new Set(sections.map((s) => s.anchor));
const missingAnchors = KNOWN_ANCHORS.filter((a) => !foundAnchors.has(a));
if (missingAnchors.length > 0) {
errors.push(`Missing expected sections: ${missingAnchors.join(", ")}`);
}
// Unexpected anchors
const knownSet = new Set<string>(KNOWN_ANCHORS);
const unexpectedAnchors = sections
.map((s) => s.anchor)
.filter((a) => !knownSet.has(a));
if (unexpectedAnchors.length > 0) {
warnings.push(`Unexpected new sections: ${unexpectedAnchors.join(", ")}`);
}
// Empty sections
for (const section of sections) {
if (section.sizeBytes === 0) {
errors.push(`Section '${section.anchor}' has 0 bytes of content`);
}
}
// Individual section size
for (const section of sections) {
if (section.sizeBytes > VALIDATION.maxSectionSize) {
warnings.push(
`Section '${section.anchor}' is ${(section.sizeBytes / 1024).toFixed(0)}KB — exceeds ${(VALIDATION.maxSectionSize / 1024).toFixed(0)}KB threshold`,
);
}
}
// Total content size
if (totalSize < VALIDATION.minTotalSize) {
errors.push(
`Total content size ${(totalSize / 1024).toFixed(0)}KB is below minimum ${(VALIDATION.minTotalSize / 1024).toFixed(0)}KB — docs may be truncated`,
);
}
if (totalSize > VALIDATION.maxTotalSize) {
warnings.push(
`Total content size ${(totalSize / 1024).toFixed(0)}KB exceeds ${(VALIDATION.maxTotalSize / 1024).toFixed(0)}KB — docs may have grown significantly`,
);
}
return {
valid: errors.length === 0,
errors,
warnings,
sectionCount: sections.length,
totalSize,
};
}
+62
View File
@@ -0,0 +1,62 @@
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
import { fetchDocs } from "../lib/fetcher.ts";
import { rm, mkdir } from "fs/promises";
import { join } from "path";
const TEST_CACHE_DIR = ".cache-test";
beforeEach(async () => {
await rm(TEST_CACHE_DIR, { recursive: true, force: true });
});
afterEach(async () => {
await rm(TEST_CACHE_DIR, { recursive: true, force: true });
});
describe("fetchDocs", () => {
it("fetches from network and caches", async () => {
const result = await fetchDocs("https://workos.com/docs/llms.txt", {
cacheDir: TEST_CACHE_DIR,
retries: 2,
});
expect(result.source).toBe("network");
expect(result.content.length).toBeGreaterThan(0);
expect(result.content).toContain("WorkOS");
// Second fetch should hit cache
const cached = await fetchDocs("https://workos.com/docs/llms.txt", {
cacheDir: TEST_CACHE_DIR,
retries: 2,
});
expect(cached.source).toBe("cache");
expect(cached.content).toBe(result.content);
});
it("bypasses expired cache", async () => {
// Pre-populate cache
await fetchDocs("https://workos.com/docs/llms.txt", {
cacheDir: TEST_CACHE_DIR,
retries: 2,
});
// Fetch with 0 TTL — should go to network
const result = await fetchDocs("https://workos.com/docs/llms.txt", {
cacheDir: TEST_CACHE_DIR,
maxAge: 0,
retries: 2,
});
expect(result.source).toBe("network");
});
it("throws on unreachable URL after retries", async () => {
await expect(
fetchDocs("https://localhost:19999/not-a-real-server", {
cacheDir: TEST_CACHE_DIR,
retries: 1,
}),
).rejects.toThrow("Failed to fetch");
});
});
+146
View File
@@ -0,0 +1,146 @@
import { describe, expect, it } from "bun:test";
import { parseSections, parseSubsections } from "../lib/parser.ts";
describe("parseSections", () => {
it("extracts sections delimited by ## Name {#anchor}", () => {
const markdown = `# Header
Some intro text.
## Widgets {#widgets}
Widget content here.
### Widget A
A details.
## Vault {#vault}
Vault content here.
`;
const sections = parseSections(markdown);
expect(sections).toHaveLength(2);
expect(sections[0].name).toBe("Widgets");
expect(sections[0].anchor).toBe("widgets");
expect(sections[0].content).toContain("Widget content here");
expect(sections[1].name).toBe("Vault");
expect(sections[1].anchor).toBe("vault");
});
it("does not split on ## inside fenced code blocks", () => {
const markdown = `## Real Section {#real}
Some content.
\`\`\`markdown
## Fake Section {#fake}
This is inside a code block.
\`\`\`
More content after code block.
## Another Section {#another}
Another content.
`;
const sections = parseSections(markdown);
expect(sections).toHaveLength(2);
expect(sections[0].anchor).toBe("real");
expect(sections[0].content).toContain("Fake Section");
expect(sections[1].anchor).toBe("another");
});
it("calculates sizeBytes and lineCount", () => {
const markdown = `## Test {#test}
Line one.
Line two.
Line three.
`;
const sections = parseSections(markdown);
expect(sections[0].lineCount).toBe(3);
expect(sections[0].sizeBytes).toBeGreaterThan(0);
});
it("handles empty content between headers", () => {
const markdown = `## Empty {#empty}
## Next {#next}
Has content.
`;
const sections = parseSections(markdown);
expect(sections).toHaveLength(2);
expect(sections[0].content).toBe("");
expect(sections[1].content).toBe("Has content.");
});
it("extracts subsections", () => {
const markdown = `## SSO {#sso}
### Getting Started
Start here.
### Configuration
Config details.
### FAQ
Questions.
`;
const sections = parseSections(markdown);
expect(sections[0].subsections).toHaveLength(3);
expect(sections[0].subsections[0].title).toBe("Getting Started");
expect(sections[0].subsections[1].title).toBe("Configuration");
expect(sections[0].subsections[2].title).toBe("FAQ");
});
});
describe("parseSubsections", () => {
it("splits on ### headings", () => {
const content = `
### First
First content.
### Second
Second content.
`;
const subs = parseSubsections(content);
expect(subs).toHaveLength(2);
expect(subs[0].title).toBe("First");
expect(subs[0].content).toBe("First content.");
expect(subs[1].title).toBe("Second");
});
it("skips ### inside code blocks", () => {
const content = `
### Real
\`\`\`
### Not a heading
\`\`\`
Real content.
### Also Real
More content.
`;
const subs = parseSubsections(content);
expect(subs).toHaveLength(2);
expect(subs[0].title).toBe("Real");
expect(subs[0].content).toContain("Not a heading");
expect(subs[1].title).toBe("Also Real");
});
it("returns empty array for content with no subsections", () => {
const subs = parseSubsections("Just plain content with no headings.");
expect(subs).toHaveLength(0);
});
});
+76
View File
@@ -0,0 +1,76 @@
import { describe, expect, it } from "bun:test";
import { validateSections } from "../lib/validator.ts";
import { KNOWN_ANCHORS } from "../lib/config.ts";
import type { Section } from "../lib/types.ts";
function makeSection(anchor: string, sizeBytes = 1000): Section {
return {
name: anchor,
anchor,
content: "x".repeat(sizeBytes),
sizeBytes,
lineCount: 10,
subsections: [],
};
}
function makeAllSections(sizeBytes = 50_000): Section[] {
return KNOWN_ANCHORS.map((a) => makeSection(a, sizeBytes));
}
describe("validateSections", () => {
it("passes with all expected sections", () => {
const result = validateSections(makeAllSections());
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(result.sectionCount).toBe(KNOWN_ANCHORS.length);
});
it("fails when section count is way off", () => {
const sections = [makeSection("sso"), makeSection("vault")];
const result = validateSections(sections);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("Expected ~24"))).toBe(true);
});
it("fails when known anchors are missing", () => {
const sections = makeAllSections().filter((s) => s.anchor !== "sso");
const result = validateSections(sections);
expect(result.valid).toBe(false);
expect(
result.errors.some((e) => e.includes("Missing expected sections: sso")),
).toBe(true);
});
it("warns on unexpected anchors", () => {
const sections = [...makeAllSections(), makeSection("new-feature")];
const result = validateSections(sections);
expect(
result.warnings.some((w) =>
w.includes("Unexpected new sections: new-feature"),
),
).toBe(true);
});
it("fails on empty section content", () => {
const sections = makeAllSections();
sections[0] = { ...sections[0], sizeBytes: 0 };
const result = validateSections(sections);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("0 bytes"))).toBe(true);
});
it("warns on oversized section", () => {
const sections = makeAllSections();
sections[0] = { ...sections[0], sizeBytes: 700_000 };
const result = validateSections(sections);
expect(result.warnings.some((w) => w.includes("exceeds"))).toBe(true);
});
it("fails when total size is too small", () => {
const sections = makeAllSections(100);
const result = validateSections(sections);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.includes("below minimum"))).toBe(true);
});
});
+123
View File
@@ -0,0 +1,123 @@
---
name: workos-authkit-base
description: Architectural reference for WorkOS AuthKit integrations. Fetch README first for implementation details.
---
# WorkOS AuthKit Base Template
## First Action: Fetch README
Before any implementation, fetch the framework-specific README:
```
WebFetch: {sdk-package-name} README from npmjs.com or GitHub
```
README is the source of truth for: install commands, imports, API usage, code patterns.
## Task Structure (Required)
| Phase | Task | Blocked By | Purpose |
| ----- | --------- | ------------------ | --------------------------------- |
| 1 | preflight | - | Verify env vars, detect framework |
| 2 | install | preflight | Install SDK package |
| 3 | callback | install | Create OAuth callback route |
| 4 | provider | install | Setup auth context/middleware |
| 5 | ui | callback, provider | Add sign-in/out UI |
| 6 | verify | ui | Build confirmation |
## Decision Trees
### Package Manager Detection
```
pnpm-lock.yaml? → pnpm
yarn.lock? → yarn
bun.lockb? → bun
else → npm
```
### Provider vs Middleware
```
Client-side framework? → AuthKitProvider wraps app
Server-side framework? → Middleware handles sessions
Hybrid (Next.js)? → Both may be needed
```
### Callback Route Location
Extract path from `WORKOS_REDIRECT_URI` → create route at that exact path.
## Environment Variables
| Variable | Purpose | When Required |
| ------------------------ | ------------------------------ | ------------- |
| `WORKOS_API_KEY` | Server authentication | Server SDKs |
| `WORKOS_CLIENT_ID` | Client identification | All SDKs |
| `WORKOS_REDIRECT_URI` | OAuth callback URL | Server SDKs |
| `WORKOS_COOKIE_PASSWORD` | Session encryption (32+ chars) | Server SDKs |
Note: Some frameworks use prefixed variants (e.g., `NEXT_PUBLIC_*`). Check README.
## Verification Checklists
### After Install
- [ ] SDK package installed in node_modules
- [ ] No install errors in output
### After Callback Route
- [ ] Route file exists at path matching `WORKOS_REDIRECT_URI`
- [ ] Imports SDK callback handler (not custom OAuth)
### After Provider/Middleware
- [ ] Provider wraps entire app (client-side)
- [ ] Middleware configured in correct location (server-side)
### After UI
- [ ] Home page shows conditional auth state
- [ ] Uses SDK functions for sign-in/out URLs
### Final Verification
- [ ] Build completes with exit code 0
- [ ] No import resolution errors
## Error Recovery
### Module not found
- [ ] Verify install completed successfully
- [ ] Verify SDK exists in node_modules
- [ ] Re-run install if missing
### Build import errors
- [ ] Delete `node_modules`, reinstall
- [ ] Verify package.json has SDK dependency
### Invalid redirect URI
- [ ] Compare route path to `WORKOS_REDIRECT_URI`
- [ ] Paths must match exactly
### Cookie password error
- [ ] Verify `WORKOS_COOKIE_PASSWORD` is 32+ characters
- [ ] Generate new: `openssl rand -base64 32`
### Auth state not persisting
- [ ] Verify provider wraps entire app
- [ ] Check middleware is in correct location
## Critical Rules
1. **Install SDK before writing imports** - never create import statements for uninstalled packages
2. **Use SDK functions** - never construct OAuth URLs manually
3. **Follow README patterns** - SDK APIs change between versions
4. **Extract callback path from env** - don't hardcode `/auth/callback`
+217
View File
@@ -0,0 +1,217 @@
---
name: workos-authkit-nextjs
description: Integrate WorkOS AuthKit with Next.js App Router (13+). Server-side rendering required.
---
# WorkOS AuthKit for Next.js
## Step 1: Fetch SDK Documentation (BLOCKING)
**STOP. Do not proceed until complete.**
WebFetch: `https://github.com/workos/authkit-nextjs/blob/main/README.md`
The README is the source of truth. If this skill conflicts with README, follow README.
## Step 2: Pre-Flight Validation
### Project Structure
- Confirm `next.config.js` or `next.config.mjs` exists
- Confirm `package.json` contains `"next"` dependency
### Environment Variables
Check `.env.local` for:
- `WORKOS_API_KEY` - starts with `sk_`
- `WORKOS_CLIENT_ID` - starts with `client_`
- `NEXT_PUBLIC_WORKOS_REDIRECT_URI` - valid callback URL
- `WORKOS_COOKIE_PASSWORD` - 32+ characters
## Step 3: Install SDK
Detect package manager, install SDK package from README.
**Verify:** SDK package exists in node_modules before continuing.
## Step 4: Version Detection (Decision Tree)
Read Next.js version from `package.json`:
```
Next.js version?
|
+-- 16+ --> Create proxy.ts at project root
|
+-- 15 --> Create middleware.ts (cookies() is async - handlers must await)
|
+-- 13-14 --> Create middleware.ts (cookies() is sync)
```
**Critical:** File MUST be at project root (or `src/` if using src directory). Never in `app/`.
**Next.js 15+ async note:** All route handlers and middleware accessing cookies must be async and properly await cookie operations. This is a breaking change from Next.js 14.
Middleware/proxy code: See README for `authkitMiddleware()` export pattern.
### Existing Middleware (IMPORTANT)
If `middleware.ts` already exists with custom logic (rate limiting, logging, headers, etc.), use the **`authkit()` composable function** instead of `authkitMiddleware`.
**Pattern for composing with existing middleware:**
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { authkit, handleAuthkitHeaders } from '@workos-inc/authkit-nextjs';
export default async function middleware(request: NextRequest) {
// 1. Get auth session and headers from AuthKit
const { session, headers, authorizationUrl } = await authkit(request);
const { pathname } = request.nextUrl;
// 2. === YOUR EXISTING MIDDLEWARE LOGIC ===
// Rate limiting, logging, custom headers, etc.
const rateLimitResult = checkRateLimit(request);
if (!rateLimitResult.allowed) {
return new NextResponse('Too Many Requests', { status: 429 });
}
// 3. Protect routes - redirect to auth if needed
if (pathname.startsWith('/dashboard') && !session.user && authorizationUrl) {
return handleAuthkitHeaders(request, headers, { redirect: authorizationUrl });
}
// 4. Continue with AuthKit headers properly handled
return handleAuthkitHeaders(request, headers);
}
```
**Key functions:**
- `authkit(request)` - Returns `{ session, headers, authorizationUrl }` for composition
- `handleAuthkitHeaders(request, headers, options?)` - Ensures AuthKit headers pass through correctly
- For rewrites, use `partitionAuthkitHeaders()` and `applyResponseHeaders()` (see README)
**Critical:** Always return via `handleAuthkitHeaders()` to ensure `withAuth()` works in pages.
## Step 5: Create Callback Route
Parse `NEXT_PUBLIC_WORKOS_REDIRECT_URI` to determine route path:
```
URI path --> Route location
/auth/callback --> app/auth/callback/route.ts
/callback --> app/callback/route.ts
```
Use `handleAuth()` from SDK. Do not write custom OAuth logic.
**CRITICAL for Next.js 15+:** The route handler MUST be async and properly await handleAuth():
```typescript
// CORRECT - Next.js 15+ requires async route handlers
export const GET = handleAuth();
// If handleAuth returns a function, ensure it's awaited in request context
```
Check README for exact usage. If build fails with "cookies outside request scope", the handler is likely missing async/await.
## Step 6: Provider Setup (REQUIRED)
**CRITICAL:** You MUST wrap the app in `AuthKitProvider` in `app/layout.tsx`.
This is required for:
- Client-side auth state via `useAuth()` hook
- Consistent auth UX across client/server boundaries
- Proper migration from Auth0 (which uses client-side auth)
```tsx
// app/layout.tsx
import { AuthKitProvider } from '@workos-inc/authkit-nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<AuthKitProvider>{children}</AuthKitProvider>
</body>
</html>
);
}
```
Check README for exact import path - it may be a subpath export like `@workos-inc/authkit-nextjs/components`.
**Do NOT skip this step** even if using server-side auth patterns elsewhere.
## Step 7: UI Integration
Add auth UI to `app/page.tsx` using SDK functions. See README for `getUser`, `getSignInUrl`, `signOut` usage.
## Verification Checklist (ALL MUST PASS)
Run these commands to confirm integration. **Do not mark complete until all pass:**
```bash
# 1. Check middleware/proxy exists (one should match)
ls proxy.ts middleware.ts src/proxy.ts src/middleware.ts 2>/dev/null
# 2. CRITICAL: Check AuthKitProvider is in layout (REQUIRED)
grep "AuthKitProvider" app/layout.tsx || echo "FAIL: AuthKitProvider missing from layout"
# 3. Check callback route exists
find app -name "route.ts" -path "*/callback/*"
# 4. Build succeeds
npm run build
```
**If check #2 fails:** Go back to Step 6 and add AuthKitProvider. This is not optional.
## Error Recovery
### "cookies was called outside a request scope" (Next.js 15+)
**Most common cause:** Route handler not properly async or missing await.
Fix for callback route:
1. Check that `handleAuth()` is exported directly: `export const GET = handleAuth();`
2. If using custom wrapper, ensure it's `async` and awaits any cookie operations
3. Verify authkit-nextjs SDK version supports Next.js 15+ (check README for compatibility)
4. **Never** call `cookies()` at module level - only inside request handlers
This error causes OAuth codes to expire ("invalid_grant"), so fix the handler first.
### "middleware.ts not found"
- Check: File at project root or `src/`, not inside `app/`
- Check: Filename matches Next.js version (proxy.ts for 16+, middleware.ts for 13-15)
### "Cannot use getUser in client component"
- Check: Component has no `'use client'` directive, or
- Check: Move auth logic to server component/API route
### "Module not found" for SDK import
- Check: SDK installed before writing imports
- Check: SDK package directory exists in node_modules
### "withAuth route not covered by middleware"
- Check: Middleware/proxy file exists at correct location
- Check: Matcher config includes the route path
### Build fails after AuthKitProvider
- Check: README for correct import path (may be subpath export)
- Check: No client/server boundary violations
### NEXT*PUBLIC* prefix issues
- Client components need `NEXT_PUBLIC_*` prefix
- Server components use plain env var names
+107
View File
@@ -0,0 +1,107 @@
---
name: workos-authkit-react-router
description: Integrate WorkOS AuthKit with React Router applications. Supports v6 and v7 (Framework, Data, Declarative modes). Use when project uses react-router, react-router-dom, or mentions React Router authentication.
---
# WorkOS AuthKit for React Router
## Decision Tree
```
1. Fetch README (BLOCKING)
2. Detect router mode
3. Follow README for that mode
4. Verify with checklist below
```
## Phase 1: Fetch SDK Documentation (BLOCKING)
**STOP - Do not write any code until this completes.**
WebFetch: `https://github.com/workos/authkit-react-router/blob/main/README.md`
The README is the source of truth. If this skill conflicts with README, **follow the README**.
## Phase 2: Detect Router Mode
| Mode | Detection Signal | Key Indicator |
| -------------- | ------------------------------- | ------------------------- |
| v7 Framework | `react-router.config.ts` exists | Routes in `app/routes/` |
| v7 Data | `createBrowserRouter` in source | Loaders in route config |
| v7 Declarative | `<BrowserRouter>` component | Routes as JSX, no loaders |
| v6 | package.json version `"6.x"` | Similar to v7 Declarative |
**Detection order:**
1. Check for `react-router.config.ts` (Framework mode)
2. Grep for `createBrowserRouter` (Data mode)
3. Check package.json version (v6 vs v7)
4. Default to Declarative if v7 with `<BrowserRouter>`
## Phase 3: Follow README
Based on detected mode, apply the corresponding README section. The README contains current API signatures and code patterns.
## Critical Distinctions
### authLoader vs authkitLoader
| Function | Purpose | Where to use |
| --------------- | ------------------------- | ---------------------- |
| `authLoader` | OAuth callback handler | Callback route ONLY |
| `authkitLoader` | Fetch user data in routes | Any route needing auth |
**Common mistake:** Using `authkitLoader` for callback route. Use `authLoader()`.
### Root Route Requirement
Auth loader MUST be on root route for child routes to access auth context.
**Wrong:** Auth loader only on `/dashboard`
**Right:** Auth loader on `/` (root), children inherit context
## Environment Variables
Required in `.env` or `.env.local`:
- `WORKOS_API_KEY` - starts with `sk_`
- `WORKOS_CLIENT_ID` - starts with `client_`
- `WORKOS_REDIRECT_URI` - full URL (e.g., `http://localhost:3000/auth/callback`)
- `WORKOS_COOKIE_PASSWORD` - 32+ chars (server modes only)
## Verification Checklist
After implementation, verify:
- [ ] SDK installed in node_modules (package name from README)
- [ ] Callback route path matches `WORKOS_REDIRECT_URI` path segment
- [ ] Auth loader/provider on root route (not just child routes)
- [ ] Build succeeds: `npm run build` exits 0
- [ ] Correct mode pattern applied (loaders vs hooks)
## Error Recovery
### "loader is not a function"
**Cause:** Using loader pattern in Declarative/v6 mode
**Fix:** Declarative/v6 modes use `AuthKitProvider` + `useAuth` hook, not loaders
### Auth state not available in child routes
**Cause:** Auth loader missing from root route
**Fix:** Add `authkitLoader` (or `AuthKitProvider`) to root route so children inherit context
### useAuth returns undefined
**Cause:** Missing `AuthKitProvider` wrapper
**Fix:** Wrap app with `AuthKitProvider` (required for Declarative/v6 modes)
### Callback route 404
**Cause:** Route path mismatch with `WORKOS_REDIRECT_URI`
**Fix:** Extract exact path from env var, create route at that path
### "Module not found" for SDK
**Cause:** SDK not installed
**Fix:** Install SDK, wait for completion, verify `node_modules` before writing imports
+91
View File
@@ -0,0 +1,91 @@
---
name: workos-authkit-react
description: Integrate WorkOS AuthKit with React single-page applications. Client-side only authentication. Use when the project is a React SPA without Next.js or React Router.
---
# WorkOS AuthKit for React (SPA)
## Decision Tree
```
START
├─► Fetch README (BLOCKING)
│ github.com/workos/authkit-react/blob/main/README.md
│ README is source of truth. Stop if fetch fails.
├─► Detect Build Tool
│ ├─ vite.config.ts exists? → Vite
│ └─ otherwise → Create React App
├─► Set Env Var Prefix
│ ├─ Vite → VITE_WORKOS_CLIENT_ID
│ └─ CRA → REACT_APP_WORKOS_CLIENT_ID
└─► Implement per README
```
## Critical: Build Tool Detection
| Marker File | Build Tool | Env Prefix | Access Pattern |
| ------------------------- | ---------- | ------------ | ------------------------- |
| `vite.config.ts` | Vite | `VITE_` | `import.meta.env.VITE_*` |
| `craco.config.js` or none | CRA | `REACT_APP_` | `process.env.REACT_APP_*` |
**Wrong prefix = undefined values at runtime.** This is the #1 integration failure.
## Key Clarification: No Callback Route
The React SDK handles OAuth callbacks **internally** via AuthKitProvider.
- No server-side callback route needed
- SDK intercepts redirect URI client-side
- Token exchange happens automatically
Just ensure redirect URI env var matches WorkOS Dashboard exactly.
## Required Environment Variables
```
{PREFIX}WORKOS_CLIENT_ID=client_...
{PREFIX}WORKOS_REDIRECT_URI=http://localhost:5173/callback
```
No `WORKOS_API_KEY` needed. Client-side only SDK.
## Verification Checklist
- [ ] README fetched and read
- [ ] Build tool detected correctly
- [ ] Env var prefix matches build tool
- [ ] `.env` or `.env.local` has required vars
- [ ] No `next` dependency (wrong skill)
- [ ] No `react-router` dependency (wrong skill)
- [ ] AuthKitProvider wraps app root
- [ ] `pnpm build` exits 0
## Error Recovery
### "clientId is required"
**Cause:** Env var inaccessible (wrong prefix)
Check: Does prefix match build tool? Vite needs `VITE_`, CRA needs `REACT_APP_`.
### Auth state lost on refresh
**Cause:** Token persistence issue
Check: Browser dev tools → Application → Local Storage. SDK stores tokens here automatically.
### useAuth returns undefined
**Cause:** Component outside provider tree
Check: Entry file (`main.tsx` or `index.tsx`) wraps `<App />` in `<AuthKitProvider>`.
### Callback redirect fails
**Cause:** URI mismatch
Check: Env var redirect URI exactly matches WorkOS Dashboard → Redirects configuration.
@@ -0,0 +1,263 @@
---
name: workos-authkit-tanstack-start
description: Integrate WorkOS AuthKit with TanStack Start applications. Full-stack TypeScript with server functions. Use when project uses TanStack Start, @tanstack/start, or vinxi.
---
# WorkOS AuthKit for TanStack Start
## Decision Tree
```
1. Fetch README (BLOCKING)
├── Extract package name from install command
└── README is source of truth for ALL code patterns
2. Detect directory structure
├── src/ (TanStack Start v1.132+, default)
└── app/ (legacy vinxi-based projects)
3. Follow README install/setup exactly
└── Do not invent commands or patterns
```
## Fetch SDK Documentation (BLOCKING)
**STOP - Do not proceed until complete.**
WebFetch: `https://github.com/workos/authkit-tanstack-start/blob/main/README.md`
From README, extract:
1. Package name: `@workos/authkit-tanstack-react-start`
2. Use that exact name for all imports
**README overrides this skill if conflict.**
## Pre-Flight Checklist
- [ ] README fetched and package name extracted
- [ ] `@tanstack/start` or `@tanstack/react-start` in package.json
- [ ] Identify directory structure: `src/` (modern) or `app/` (legacy)
- [ ] Environment variables set (see below)
## Directory Structure Detection
**Modern TanStack Start (v1.132+)** uses `src/`:
```
src/
├── start.ts # Middleware config (CRITICAL)
├── router.tsx # Router setup
├── routes/
│ ├── __root.tsx # Root layout
│ ├── api.auth.callback.tsx # OAuth callback (flat route)
│ └── ...
```
**Legacy (vinxi-based)** uses `app/`:
```
app/
├── start.ts or router.tsx
├── routes/
│ └── api/auth/callback.tsx # OAuth callback (nested route)
```
**Detection:**
```bash
ls src/routes 2>/dev/null && echo "Modern (src/)" || echo "Legacy (app/)"
```
## Environment Variables
| Variable | Format | Required |
| ------------------------ | ------------ | -------- |
| `WORKOS_API_KEY` | `sk_...` | Yes |
| `WORKOS_CLIENT_ID` | `client_...` | Yes |
| `WORKOS_REDIRECT_URI` | Full URL | Yes |
| `WORKOS_COOKIE_PASSWORD` | 32+ chars | Yes |
Generate password if missing: `openssl rand -base64 32`
Default redirect URI: `http://localhost:3000/api/auth/callback`
## Middleware Configuration (CRITICAL)
**authkitMiddleware MUST be configured or auth will fail silently.**
Create or update `src/start.ts` (or `app/start.ts` for legacy):
```typescript
import { authkitMiddleware } from '@workos/authkit-tanstack-react-start';
export default {
requestMiddleware: [authkitMiddleware()],
};
```
Alternative pattern with createStart:
```typescript
import { createStart } from '@tanstack/react-start';
import { authkitMiddleware } from '@workos/authkit-tanstack-react-start';
export default createStart({
requestMiddleware: [authkitMiddleware()],
});
```
### Verification Checklist
- [ ] `authkitMiddleware` imported from `@workos/authkit-tanstack-react-start`
- [ ] Middleware in `requestMiddleware` array
- [ ] File exports the config (default export or named `startInstance`)
Verify: `grep -r "authkitMiddleware" src/ app/ 2>/dev/null`
## Callback Route (CRITICAL)
Path must match `WORKOS_REDIRECT_URI`. For `/api/auth/callback`:
**Modern (flat routes):** `src/routes/api.auth.callback.tsx`
**Legacy (nested routes):** `app/routes/api/auth/callback.tsx`
```typescript
import { createFileRoute } from '@tanstack/react-router';
import { handleCallbackRoute } from '@workos/authkit-tanstack-react-start';
export const Route = createFileRoute('/api/auth/callback')({
server: {
handlers: {
GET: handleCallbackRoute(),
},
},
});
```
**Key points:**
- Use `handleCallbackRoute()` - do not write custom OAuth logic
- Route path string must match the URI path exactly
- This is a server-only route (no component needed)
## Protected Routes
Use `getAuth()` in route loaders to check authentication:
```typescript
import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start';
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const { user } = await getAuth();
if (!user) {
const signInUrl = await getSignInUrl();
throw redirect({ href: signInUrl });
}
return { user };
},
component: Dashboard,
});
```
## Sign Out Route
```typescript
import { createFileRoute, redirect } from '@tanstack/react-router';
import { signOut } from '@workos/authkit-tanstack-react-start';
export const Route = createFileRoute('/signout')({
loader: async () => {
await signOut();
throw redirect({ href: '/' });
},
});
```
## Client-Side Hooks (Optional)
Only needed if you want reactive auth state in components.
**1. Add AuthKitProvider to root:**
```typescript
// src/routes/__root.tsx
import { AuthKitProvider } from '@workos/authkit-tanstack-react-start/client';
function RootComponent() {
return (
<AuthKitProvider>
<Outlet />
</AuthKitProvider>
);
}
```
**2. Use hooks in components:**
```typescript
import { useAuth } from '@workos/authkit-tanstack-react-start/client';
function Profile() {
const { user, isLoading } = useAuth();
// ...
}
```
**Note:** Server-side `getAuth()` is preferred for most use cases.
## Error Recovery
### "AuthKit middleware is not configured"
**Cause:** `authkitMiddleware()` not in start.ts
**Fix:** Create/update `src/start.ts` with middleware config
**Verify:** `grep -r "authkitMiddleware" src/`
### "Module not found" for SDK
**Cause:** Wrong package name or not installed
**Fix:** `pnpm add @workos/authkit-tanstack-react-start`
**Verify:** `ls node_modules/@workos/authkit-tanstack-react-start`
### Callback 404
**Cause:** Route file path doesn't match WORKOS_REDIRECT_URI
**Fix:**
- URI `/api/auth/callback` → file `src/routes/api.auth.callback.tsx` (flat) or `app/routes/api/auth/callback.tsx` (nested)
- Route path string in `createFileRoute()` must match exactly
### getAuth returns undefined user
**Cause:** Middleware not configured or not running
**Fix:** Ensure `authkitMiddleware()` is in start.ts requestMiddleware array
### "Cookie password too short"
**Cause:** WORKOS_COOKIE_PASSWORD < 32 chars
**Fix:** `openssl rand -base64 32`, update .env
### Build fails with route type errors
**Cause:** Route tree not regenerated after adding routes
**Fix:** `pnpm dev` to regenerate `routeTree.gen.ts`
## SDK Exports Reference
**Server (main export):**
- `authkitMiddleware()` - Request middleware
- `handleCallbackRoute()` - OAuth callback handler
- `getAuth()` - Get current session
- `signOut()` - Sign out user
- `getSignInUrl()` / `getSignUpUrl()` - Auth URLs
- `switchToOrganization()` - Change org context
**Client (`/client` subpath):**
- `AuthKitProvider` - Context provider
- `useAuth()` - Auth state hook
- `useAccessToken()` - Token management
+83
View File
@@ -0,0 +1,83 @@
---
name: workos-authkit-vanilla-js
description: Integrate WorkOS AuthKit with vanilla JavaScript applications. No framework required, browser-only. Use when project is plain HTML/JS, doesn't use React/Vue/etc, or mentions vanilla JavaScript authentication.
---
# WorkOS AuthKit for Vanilla JavaScript
## Decision Tree
### Step 1: Fetch README (BLOCKING)
WebFetch: `https://github.com/workos/authkit-js/blob/main/README.md`
**README is source of truth.** If this skill conflicts, follow README.
### Step 2: Detect Project Type
```
Has package.json with build tool (Vite, webpack, Parcel)?
YES -> Bundled project (npm install)
NO -> CDN/Static project (script tag)
```
### Step 3: Follow README Installation
- **Bundled**: Use package manager install from README
- **CDN**: Use unpkg script tag from README
### Step 4: Implement Per README
Follow README examples for:
- Client initialization
- Sign in/out handlers
- User state management
## Critical API Quirk
`createClient()` is **async** - returns a Promise, not a client directly.
```javascript
// CORRECT
const authkit = await createClient(clientId);
```
## Verification Checklist
- [ ] README fetched and read before writing code
- [ ] Project type detected (bundled vs CDN)
- [ ] SDK installed/script added
- [ ] `createClient()` called with `await`
- [ ] Client ID provided (env var or hardcoded)
- [ ] Sign in called from user gesture (click handler)
- [ ] No console errors on page load
- [ ] Auth UI updates on sign in/out
## Environment Variables
**Bundled projects only:**
- Vite: `VITE_WORKOS_CLIENT_ID`
- Webpack: `REACT_APP_WORKOS_CLIENT_ID` or custom
- No `WORKOS_API_KEY` needed (client-side SDK)
## Error Recovery
| Error | Cause | Fix |
| -------------------------------- | ------------------- | ------------------------------------------------------ |
| `WorkOS is not defined` | CDN not loaded | Add script to `<head>` before your code |
| `createClient is not a function` | Wrong import | npm: check import path; CDN: use `WorkOS.createClient` |
| `clientId is required` | Undefined env var | Check env prefix matches build tool |
| CORS errors | `file://` protocol | Use local dev server (`npx serve`) |
| Popup blocked | Not user gesture | Call `signIn()` only from click handler |
| Auth state lost | Token not persisted | Check localStorage in dev tools |
## Task Flow
1. **preflight**: Fetch README, detect project type, verify env vars
2. **install**: Add SDK per project type
3. **callback**: SDK handles internally (no server route needed)
4. **provider**: Initialize client with `await createClient()`
5. **ui**: Add auth buttons and state display
6. **verify**: Build (if bundled), check console
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["bun-types"]
},
"include": ["scripts/**/*.ts"],
"exclude": ["node_modules"]
}