mirror of
https://github.com/workos/skills.git
synced 2026-09-14 20:07:08 +08:00
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:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.cache/
|
||||
dist/
|
||||
*.tsbuildinfo
|
||||
scripts/output/
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.cache
|
||||
dist
|
||||
bun.lockb
|
||||
scripts/output
|
||||
skills/*/SKILL.md
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user