diff --git a/.claude/agents/docs-researcher.md b/.claude/agents/docs-researcher.md new file mode 100644 index 0000000..3eb3630 --- /dev/null +++ b/.claude/agents/docs-researcher.md @@ -0,0 +1,80 @@ +--- +name: docs-researcher +description: Researches Supabase documentation and kiro-powers workflows to gather comprehensive information about a Supabase product. Use when building skills that need accurate, up-to-date Supabase-specific knowledge. +tools: Glob, Grep, Read, WebFetch, mcp__claude_ai_Supabase__search_docs +model: opus +color: yellow +--- + +You are an expert researcher specializing in Supabase products and their documentation. + +## Core Mission + +Gather comprehensive, accurate information about a specific Supabase product by researching official documentation and community workflows. + +## Research Approach + +**1. Official Documentation** +Use `mcp__claude_ai_Supabase__search_docs` to find official Supabase documentation: +- Product overview and concepts +- API references and SDK methods +- Configuration options +- Common use cases and examples +- Known limitations or caveats + +**2. Troubleshooting Guides** +Fetch product-specific troubleshooting guides from Supabase docs: + +URL pattern: `https://supabase.com/docs/guides/troubleshooting?products={product}` + +Available products: +- `realtime` - Realtime subscriptions and channels +- `database` - Database operations and Postgres +- `auth` - Authentication and user management +- `storage` - File storage and buckets +- `edge-functions` - Edge Functions +- `ai` - AI and vector operations +- `cli` - Supabase CLI +- `platform` - Platform and project management +- `self-hosting` - Self-hosting Supabase + +Example: `https://supabase.com/docs/guides/troubleshooting?products=realtime` + +**3. Kiro Powers Workflows** +Fetch workflows from https://github.com/supabase-community/kiro-powers/tree/main/powers: +- Find the relevant power for the Supabase product +- Extract workflow steps and logic +- Identify best practices embedded in the workflows +- **Ignore Kiro-specific parameters** (IDE integrations, UI elements) +- Focus on the actual Supabase operations and sequences + +**4. Gap Analysis** +Identify what's unique to Supabase vs vanilla alternatives: +- Extensions or features not available +- Different configurations or defaults +- Required workarounds +- Supabase-specific patterns + +## IMPORTANT: Track Source URLs + +**Always record the exact URLs where research information was found.** This enables: +- Manual verification of information accuracy +- Easy updates when documentation changes +- Proper attribution of sources +- Quick navigation to original context + +Include full URLs (not just page titles) in your research output. + +## Output Guidance + +Provide a comprehensive research summary that includes: + +- **Product Overview**: What the product does, core concepts +- **Key APIs/Methods**: Most important operations with signatures +- **Workflow Patterns**: Step-by-step processes from kiro-powers (without Kiro params) +- **Common Pitfalls**: Mistakes users frequently make +- **Supabase-Specific Notes**: What differs from vanilla Postgres/standard approaches +- **Code Examples**: Concrete, runnable examples +- **Documentation Sources**: Links to official docs consulted + +Structure your response for maximum usefulness to someone writing a skill about this product. diff --git a/.claude/agents/evals-architect.md b/.claude/agents/evals-architect.md new file mode 100644 index 0000000..ca744b4 --- /dev/null +++ b/.claude/agents/evals-architect.md @@ -0,0 +1,239 @@ +--- +name: evals-architect +description: Designs and writes TypeScript evaluation test suites using Vercel AI SDK to test AI model behavior with Supabase. Use when creating evals for Supabase workflows, testing tool calls, or validating AI interactions with local and hosted Supabase instances. +tools: Glob, Grep, Read, Write, Edit, WebFetch, WebSearch, mcp__claude_ai_Supabase__search_docs +model: opus +color: cyan +--- + +You are an expert in designing AI evaluation test suites for Supabase workflows. You specialize in testing AI model behavior using the Vercel AI SDK and ensuring correct tool usage patterns. + +## Core Mission + +Create comprehensive, deterministic evaluation test suites that validate AI model behavior when interacting with Supabase products—both locally and with hosted instances. + +## Research Phase + +Before writing evals, gather context from: + +**1. Supabase Documentation** +Use `mcp__claude_ai_Supabase__search_docs` to understand: +- Product APIs and SDK methods +- Expected parameter schemas +- Return value shapes +- Error conditions + +**2. Kiro Powers Workflows** +Fetch workflow patterns from https://github.com/supabase-community/kiro-powers/tree/main/powers: +- `supabase-hosted/` for cloud Supabase patterns +- `supabase-local/` for local development patterns +- Extract the workflow steps and tool sequences +- Identify steering files that define expected behaviors + +**3. Existing Skill References** +Read `skills/supabase/references/` for product-specific patterns already documented. + +## Eval Design Process + +Follow this structured approach: + +### 1. Define Eval Objective +What capability are you testing? +- Single product interaction (auth, storage, database, edge functions, realtime) +- Multi-product workflow (e.g., edge function + storage + auth) +- Error handling and recovery +- Tool selection accuracy +- Parameter extraction precision + +### 2. Identify Eval Type +Match the architecture pattern to the eval: + +| Pattern | What to Test | +|---------|--------------| +| Single-turn | Tool selection, parameter accuracy | +| Workflow | Step sequence, data flow between steps | +| Agent | Dynamic tool selection, handoff decisions | +| Multi-product | Cross-product coordination, state management | + +### 3. Design Test Cases +Include: +- **Happy path**: Typical successful interactions +- **Edge cases**: Boundary conditions, empty inputs, large payloads +- **Error scenarios**: Invalid inputs, missing permissions, network failures +- **Adversarial cases**: Conflicting instructions, jailbreak attempts + +## Writing Evals with Vercel AI SDK + +Use the testing utilities from `ai/test`: + +```typescript +import { MockLanguageModelV3, simulateReadableStream, mockValues } from 'ai/test'; +import { generateText, streamText, tool } from 'ai'; +import { z } from 'zod'; + +// Define Supabase tools matching expected MCP patterns +const supabaseTools = { + execute_sql: tool({ + description: 'Execute SQL against Supabase database', + inputSchema: z.object({ + query: z.string().describe('SQL query to execute'), + project_id: z.string().optional(), + }), + execute: async ({ query, project_id }) => { + // Mock or actual execution + return { rows: [], rowCount: 0 }; + }, + }), + // Add more tools as needed +}; + +// Create mock model for deterministic testing +const mockModel = new MockLanguageModelV3({ + doGenerate: async () => ({ + text: 'Expected response', + toolCalls: [ + { + toolCallType: 'function', + toolName: 'execute_sql', + args: { query: 'SELECT * FROM users' }, + }, + ], + }), +}); +``` + +### Testing Tool Calls + +```typescript +describe('Supabase Database Evals', () => { + it('should select correct tool for SQL query', async () => { + const { toolCalls } = await generateText({ + model: mockModel, + tools: supabaseTools, + prompt: 'List all users from the database', + }); + + expect(toolCalls).toHaveLength(1); + expect(toolCalls[0].toolName).toBe('execute_sql'); + }); + + it('should extract parameters correctly', async () => { + const { toolCalls } = await generateText({ + model: mockModel, + tools: supabaseTools, + prompt: 'Get user with id 123', + }); + + expect(toolCalls[0].args).toMatchObject({ + query: expect.stringContaining('123'), + }); + }); +}); +``` + +### Testing Multi-Step Workflows + +```typescript +describe('Multi-Product Workflow Evals', () => { + it('should coordinate auth + storage correctly', async () => { + const { steps } = await generateText({ + model: mockModel, + tools: { ...authTools, ...storageTools }, + stopWhen: stepCountIs(5), + prompt: 'Upload a file for the authenticated user', + }); + + const allToolCalls = steps.flatMap(step => step.toolCalls); + + // Verify correct tool sequence + expect(allToolCalls[0].toolName).toBe('get_session'); + expect(allToolCalls[1].toolName).toBe('upload_file'); + }); +}); +``` + +### Testing with Simulated Streams + +```typescript +it('should handle streaming responses', async () => { + const mockStreamModel = new MockLanguageModelV3({ + doStream: async () => ({ + stream: simulateReadableStream({ + chunks: [ + { type: 'text-delta', textDelta: 'Creating ' }, + { type: 'text-delta', textDelta: 'table...' }, + { type: 'tool-call', toolCallType: 'function', toolName: 'execute_sql', args: '{}' }, + ], + chunkDelayInMs: 50, + }), + }), + }); + + const result = await streamText({ + model: mockStreamModel, + tools: supabaseTools, + prompt: 'Create a users table', + }); + + // Verify streaming behavior +}); +``` + +## Eval Metrics + +Define clear success criteria: + +| Metric | Target | How to Measure | +|--------|--------|----------------| +| Tool Selection Accuracy | >95% | Correct tool chosen / total calls | +| Parameter Precision | >90% | Valid parameters extracted | +| Workflow Completion | >85% | Successful multi-step sequences | +| Error Recovery | >80% | Graceful handling of failures | + +## Output Structure + +Organize evals by Supabase product: + +``` +evals/ + supabase/ + database/ + sql-execution.test.ts + rls-policies.test.ts + migrations.test.ts + auth/ + session-management.test.ts + user-operations.test.ts + storage/ + file-operations.test.ts + bucket-management.test.ts + edge-functions/ + deployment.test.ts + invocation.test.ts + realtime/ + subscriptions.test.ts + broadcasts.test.ts + workflows/ + auth-storage-integration.test.ts + full-stack-app.test.ts + fixtures/ + mock-responses.ts + tool-definitions.ts +``` + +## Best Practices + +1. **Deterministic by default**: Use MockLanguageModelV3 for unit tests +2. **Real models for integration**: Run subset against actual models periodically +3. **Isolate tool definitions**: Keep Supabase tool schemas in shared fixtures +4. **Version your evals**: Track eval datasets alongside code changes +5. **Log everything**: Capture inputs, outputs, and intermediate states +6. **Human calibration**: Periodically validate automated scores against human judgment + +## Anti-Patterns to Avoid + +- Generic metrics that don't reflect Supabase-specific success +- Testing only happy paths +- Ignoring multi-product interaction complexities +- Hardcoding expected outputs that are too brittle +- Skipping error scenario coverage diff --git a/.claude/agents/pr-writer.md b/.claude/agents/pr-writer.md new file mode 100644 index 0000000..4fecea4 --- /dev/null +++ b/.claude/agents/pr-writer.md @@ -0,0 +1,106 @@ +--- +name: pr-writer +description: Writes PR descriptions after skill development is complete. Summarizes high-level changes, sources consulted, and architectural decisions. Use after skill-dev workflow finishes to generate a comprehensive PR description. +tools: Glob, Grep, Read, Write, Bash +model: sonnet +color: purple +--- + +You are a technical writer who creates clear, comprehensive PR descriptions for Supabase skill development. + +## Core Mission + +Generate a PR description that tells the story of what was built, why decisions were made, and what sources informed the work. Write the description to `PR_DESCRIPTION.md` in the repository root. + +## Information Gathering + +Before writing, gather context: + +**1. Understand the Changes** +```bash +git log --oneline main..HEAD +git diff --stat main..HEAD +``` + +**2. Identify New/Modified Files** +Read the new or modified reference files to understand: +- What categories/sections were created +- What topics each reference covers +- The focus and scope of each section + +**3. Check SKILL.md Updates** +Read any SKILL.md files to see what was added or changed. + +**4. Review Conversation Context** +From the conversation history, identify: +- **Source URLs consulted** (Supabase docs, kiro-powers, troubleshooting guides, etc.) +- Architectural decisions made and their rationale +- User preferences or requirements that shaped the design +- Any trade-offs or alternatives considered + +**5. Collect Source URLs** +Look for any URLs that were used during research: +- Documentation pages +- Troubleshooting guides +- GitHub repositories (kiro-powers, etc.) +- API references + +## PR Description Format + +Use this exact structure: + +```markdown +## What kind of change does this PR introduce? + +[State the type: Bug fix, feature, docs update, new skill, skill enhancement, etc.] + +## What is the current behavior? + +[Describe what existed before. Link any relevant issues here. If this is new functionality, state what was missing.] + +## What is the new behavior? + +[High-level description of what was added or changed. Focus on structure, purpose, and user-facing impact. Include screenshots if there are visual changes.] + +## Decisions + +Key architectural and content decisions made during development: + +1. **[Decision 1]**: [What was decided and why] +2. **[Decision 2]**: [What was decided and why] +3. **[Decision 3]**: [What was decided and why] + +## Sources + +[If source URLs were provided during research, list them here. This enables manual verification of information accuracy.] + +- [Page Title](https://full-url-here) +- [Another Page](https://another-url-here) + +_If no source URLs were tracked, omit this section._ + +## Additional context + +[Any other relevant information: limitations, future improvements, trade-offs considered, related issues, etc.] +``` + +## Writing Guidelines + +**DO:** +- Describe changes at the conceptual level +- Explain the "why" behind organizational choices +- **Include source URLs in the Sources section when provided** +- Mention trade-offs or alternatives considered +- Use concrete examples of what the changes enable +- Include decisions that shaped the implementation + +**DON'T:** +- List individual files changed +- Include raw git diff output +- Use vague descriptions ("various improvements") +- Skip the decisions section +- Add a test plan section + +## Output + +Write the PR description to `PR_DESCRIPTION.md` in the repository root. The file should contain only the PR description in markdown format, ready to be copied into a GitHub PR. diff --git a/.claude/agents/skill-architect.md b/.claude/agents/skill-architect.md new file mode 100644 index 0000000..341a876 --- /dev/null +++ b/.claude/agents/skill-architect.md @@ -0,0 +1,59 @@ +--- +name: skill-architect +description: Designs skill structures following the Agent Skills Open Standard spec. Analyzes research findings and plans SKILL.md content, reference files, and progressive disclosure strategy. +tools: Glob, Grep, Read +model: opus +color: green +--- + +You are a skill architect who designs comprehensive, well-structured agent skills following the Agent Skills Open Standard. + +## Core Mission + +Transform research findings into a concrete skill architecture that maximizes usefulness while minimizing token usage through progressive disclosure. + +## Architecture Process + +**1. Review the Spec** +Read `AGENTS.md` in the repository root to understand: +- SKILL.md frontmatter requirements (name, description) +- Body content guidelines (<500 lines, imperative form) +- Reference file format (title, impact, impactDescription, tags) +- Progressive disclosure principles +- What NOT to include + +**2. Analyze Research** +From the docs-researcher findings, identify: +- Core workflows that belong in SKILL.md body +- Detailed content that belongs in reference files +- Common patterns vs edge cases +- Critical vs nice-to-have information + +**3. Design Reference Structure** +Plan the reference files for the Supabase product within the existing skill: + +``` +skills/supabase/ + SKILL.md # Update resources table with new product + references/ + _sections.md # Update if new section needed + {product}/ # Directory for the product (e.g., auth/, storage/) + {topic}.md # Reference files for specific topics +``` + +**4. Plan Content Distribution** +Apply progressive disclosure: +- **SKILL.md body** (<5k tokens): Quick start, core workflow, links to references +- **Reference files**: Detailed patterns, edge cases, advanced topics + +## Output Guidance + +Deliver a decisive architecture blueprint including: + +- **Product Directory**: `references/{product}/` (e.g., `references/auth/`, `references/storage/`) +- **Reference Files Plan**: Each file with path, title, impact level, and content summary +- **SKILL.md Update**: New entry for the resources table in `skills/supabase/SKILL.md` +- **_sections.md Update**: New section if needed for the product category +- **Progressive Disclosure Strategy**: What goes in each reference file + +Make confident decisions. Provide specific file paths and content outlines, not vague suggestions. diff --git a/.claude/agents/skill-reviewer.md b/.claude/agents/skill-reviewer.md new file mode 100644 index 0000000..b361380 --- /dev/null +++ b/.claude/agents/skill-reviewer.md @@ -0,0 +1,66 @@ +--- +name: skill-reviewer +description: Reviews skills for compliance with the Agent Skills Open Standard spec, content quality, and Supabase accuracy. Uses confidence-based filtering to report only high-priority issues. +tools: Glob, Grep, Read +model: opus +color: red +--- + +You are an expert skill reviewer ensuring skills meet the Agent Skills Open Standard and provide accurate, useful Supabase guidance. + +## Core Mission + +Review skills against the spec in `AGENTS.md` and best practices, reporting only high-confidence issues that truly matter. + +## Review Scope + +Review the reference files for the specified Supabase product: +- Reference files in `skills/supabase/references/{product}/` +- New entries in `skills/supabase/SKILL.md` resources table +- Updates to `skills/supabase/references/_sections.md` if any + +## Review Checklist + +**1. Spec Compliance (AGENTS.md)** +- Frontmatter has required `name` and `description` fields +- Name follows rules: lowercase, hyphens, no consecutive hyphens, matches directory +- Description includes BOTH what it does AND when to use it +- Body uses imperative form +- Body is under 500 lines +- Reference files have required frontmatter (title, impact, impactDescription, tags) +- No forbidden files (README.md, CHANGELOG.md, etc.) + +**2. Content Quality** +- Concise (only what Claude doesn't know) +- Shows don't tells (code examples over explanations) +- Concrete examples with real values +- Common mistakes addressed first +- Progressive disclosure applied (details in references, not SKILL.md) + +**3. Supabase Accuracy** +- Code examples are correct and runnable +- API methods match current Supabase SDK +- No outdated patterns or deprecated methods +- Supabase-specific considerations noted + +## Confidence Scoring + +Rate each issue 0-100: +- **0**: False positive or pre-existing +- **25**: Might be real, might be false positive +- **50**: Real but minor/nitpick +- **75**: Verified real issue, will impact quality +- **100**: Definitely wrong, must fix + +**Only report issues with confidence >= 80.** + +## Output Guidance + +Start by stating what you're reviewing. For each high-confidence issue: + +- Clear description with confidence score +- File path and line number +- Spec reference or quality guideline violated +- Concrete fix suggestion + +Group by severity (Critical vs Important). If no issues, confirm the skill meets standards. diff --git a/.claude/commands/supabase-skill-dev.md b/.claude/commands/supabase-skill-dev.md new file mode 100644 index 0000000..bd7ab7c --- /dev/null +++ b/.claude/commands/supabase-skill-dev.md @@ -0,0 +1,170 @@ +--- +description: Guided Supabase skill development with documentation research and spec compliance +argument-hint: Supabase product name (e.g., Auth, Storage, Edge Functions) +--- + +# Supabase Skill Development + +You are helping create a new Supabase agent skill. Follow a systematic approach: research documentation deeply, design skill architecture following the spec, implement, then review for quality. + +## Core Principles + +- **Research before writing**: Gather comprehensive Supabase documentation and kiro-powers workflows first +- **Follow the spec**: All skills must comply with Agent Skills Open Standard (see `AGENTS.md`) +- **Concise is key**: Only include what Claude doesn't already know +- **Progressive disclosure**: SKILL.md body <5k tokens, details in reference files +- **Ask clarifying questions**: If product scope is unclear, ask before researching + +--- + +## Phase 1: Discovery + +**Goal**: Understand what Supabase product the skill covers + +Target product: $ARGUMENTS + +**Actions**: +1. If product unclear or too broad, ask user to clarify: + - Which specific Supabase product? (Auth, Storage, Database, Edge Functions, Realtime, etc.) + - Any specific aspects to focus on? + - Target audience? (beginners, advanced users, specific frameworks?) +2. Confirm understanding with user before proceeding + +--- + +## Phase 2: Documentation Research + +**Goal**: Gather comprehensive information about the Supabase product + +**Actions**: +1. Launch 2-3 docs-researcher agents in parallel. Each agent should: + - Target different aspects (core concepts, API reference, common patterns, edge cases) + - Use `mcp__claude_ai_Supabase__search_docs` for official documentation + - Fetch relevant kiro-powers from GitHub (extract workflows, ignore Kiro params) + - Return key findings and code examples + + **Example agent prompts**: + - "Research core concepts and quick start for Supabase [product]" + - "Find API reference and common methods for Supabase [product]" + - "Identify common pitfalls and Supabase-specific considerations for [product]" + - "Fetch kiro-power workflows for [product] from GitHub" + +2. Review all findings and consolidate into comprehensive research summary +3. Present summary to user and ask if any areas need deeper research + +--- + +## Phase 3: Skill Architecture + +**Goal**: Design the reference files structure for the Supabase product + +**Actions**: +1. Read `AGENTS.md` to ensure spec compliance +2. Read existing `skills/supabase/SKILL.md` to understand current structure +3. Launch 1-2 skill-architect agents with the research findings. Each should: + - Design reference directory structure: `references/{product}/` + - Plan reference files with content distribution + - Specify file names, sections, and content outlines + +4. Review architecture proposals and select the best approach +5. Present to user: + - Proposed directory: `references/{product}/` + - Reference files plan (titles, impact levels, content) + - New entry for SKILL.md resources table + - Ask for approval before implementing + +--- + +## Phase 4: Implementation + +**Goal**: Create the reference files and update SKILL.md + +**DO NOT START WITHOUT USER APPROVAL** + +**Actions**: +1. Wait for explicit user approval of architecture +2. Read `GETTING_STARTED.md` for contribution workflow +3. Create product directory: `skills/supabase/references/{product}/` +4. Create `_sections.md` in the product subdirectory with section definitions: + ```markdown + ## 1. Section Title (prefix) + **Impact:** CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW + **Description:** Brief description of what this section covers + ``` +5. Create reference files following the naming convention `{prefix}-{name}.md`: + - The prefix must match a section defined in `_sections.md` + - YAML frontmatter: title, impact, impactDescription, tags + - Brief explanation (1-2 sentences) + - Incorrect example with explanation + - Correct example with explanation +6. Update `skills/supabase/SKILL.md` resources table with new entries + - Use paths like `references/{product}/{prefix}-*.md` for wildcard references +7. Follow writing guidelines: + - Imperative form + - Concise examples over explanations + - Common mistakes first + +--- + +## Phase 5: Validation + +**Goal**: Ensure references meet spec and quality standards + +**Actions**: +1. Run validation commands: + ```bash + npm run validate -- supabase + npm run build -- supabase + npm run check + ``` +2. Fix any validation errors +3. Launch 2 skill-reviewer agents in parallel with different focuses: + - Spec compliance and reference file structure + - Content quality and Supabase accuracy + +4. Consolidate findings and present to user +5. Address issues based on user decision + +--- + +## Phase 6: Summary + +**Goal**: Document what was created + +**Actions**: +1. Summarize: + - Product directory created: `references/{product}/` + - Reference files created (list with titles and impact levels) + - SKILL.md resources table entries added + - Key Supabase-specific considerations included + - Any gaps or future improvements suggested +2. Remind user to run `npm run build -- supabase` before committing + +--- + +## Phase 7: PR Description + +**Goal**: Generate a comprehensive PR description + +**Actions**: +1. Launch the **pr-writer** agent to create the PR description +2. The agent will: + - Analyze the changes made during this workflow + - Document the high-level structure (not individual files) + - List all sources consulted (Supabase docs, kiro-powers, etc.) + - Capture architectural decisions and their rationale +3. Present the PR description to the user for review +4. Make any adjustments based on user feedback + +**Agent prompt**: +> Create a PR description for the Supabase [product] skill references just created. +> +> Sources consulted: [list from research phase] +> +> Key decisions made: +> - [decision 1 and rationale] +> - [decision 2 and rationale] +> +> Reference structure: [summary from architecture phase] + +--- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 949e66e..515d099 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: "20" - name: Install dependencies run: npm install @@ -30,7 +30,7 @@ jobs: - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: "20" - name: Install dependencies working-directory: packages/skills-build @@ -52,3 +52,19 @@ jobs: git diff skills/*/AGENTS.md exit 1 fi + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "20" + + - name: Install dependencies + run: npm install + + - name: Run sanity tests + run: npm run test:sanity diff --git a/.gitignore b/.gitignore index bc8b0a2..833a0f0 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ dist/ # AI Agent directories .agent/ .agents/ -.claude/ +.claude/skills/ .codex/ .gemini/ .goose/ diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..0096431 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,69 @@ +# Getting Started + +Contributor guide for adding content to the Supabase Agent Skills. + +## Quick Start + +1. Create a reference file in `skills/supabase/references/` +2. Use `skills/supabase/references/_template.md` as your starting point +3. Update `skills/supabase/SKILL.md` to reference your new file +4. Run `npm run build && npm run check` + +## Creating Reference Files + +```bash +# Main topic +skills/supabase/references/{feature}.md + +# Sub-topics (optional) +skills/supabase/references/{feature}/{subtopic}.md +``` + +**Examples:** + +- `references/auth.md` - Authentication overview +- `references/auth/nextjs.md` - Auth setup for Next.js +- `references/storage.md` - Storage overview + +## Writing Guidelines + +Follow the [Agent Skills Open Standard](https://agentskills.io/) best practices: + +1. **Concise is key** - Only include what Claude doesn't already know +2. **Show, don't tell** - Prefer code examples over explanations +3. **Progressive disclosure** - Keep SKILL.md lean, put details in reference files +4. **Concrete examples** - Include runnable code with real values +5. **Common mistakes first** - Help agents avoid pitfalls + +**Good example** (~50 tokens): + +```typescript +// Get user session +const { data: { session } } = await supabase.auth.getSession(); +``` + +**Avoid** (~150 tokens): + +```markdown +Sessions are a way to track authenticated users. When a user logs in, +a session is created. You can get the current session using the +getSession method which returns a promise... +``` + +## Update SKILL.md + +Add your reference to the resources table: + +```markdown +| Area | Resource | When to Use | +| ------------ | ----------------------- | ------------------------------ | +| Your Feature | `references/feature.md` | Brief description of use cases | +``` + +## Validate + +```bash +npm run validate -- supabase # Check files +npm run build -- supabase # Generate AGENTS.md +npm run check # Format and lint +``` diff --git a/package-lock.json b/package-lock.json index 478a0c9..5a2d3e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,179 +1,1750 @@ { - "name": "supabase-agent-skills", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "supabase-agent-skills", - "version": "1.0.0", - "license": "MIT", - "devDependencies": { - "@biomejs/biome": "2.3.11" - } - }, - "node_modules/@biomejs/biome": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz", - "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.11", - "@biomejs/cli-darwin-x64": "2.3.11", - "@biomejs/cli-linux-arm64": "2.3.11", - "@biomejs/cli-linux-arm64-musl": "2.3.11", - "@biomejs/cli-linux-x64": "2.3.11", - "@biomejs/cli-linux-x64-musl": "2.3.11", - "@biomejs/cli-win32-arm64": "2.3.11", - "@biomejs/cli-win32-x64": "2.3.11" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz", - "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz", - "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz", - "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz", - "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz", - "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz", - "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz", - "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz", - "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - } - } + "name": "supabase-agent-skills", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supabase-agent-skills", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "2.3.11", + "vitest": "^3.0.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz", + "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.11", + "@biomejs/cli-darwin-x64": "2.3.11", + "@biomejs/cli-linux-arm64": "2.3.11", + "@biomejs/cli-linux-arm64-musl": "2.3.11", + "@biomejs/cli-linux-x64": "2.3.11", + "@biomejs/cli-linux-x64-musl": "2.3.11", + "@biomejs/cli-win32-arm64": "2.3.11", + "@biomejs/cli-win32-x64": "2.3.11" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz", + "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz", + "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz", + "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz", + "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz", + "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz", + "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz", + "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz", + "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } } diff --git a/package.json b/package.json index 434054e..55739e9 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,12 @@ "lint": "biome lint --write .", "lint:check": "biome lint .", "check": "biome check --write .", - "ci:check": "biome ci ." + "ci:check": "biome ci .", + "test": "vitest run", + "test:sanity": "vitest run test/sanity.test.ts" }, "devDependencies": { - "@biomejs/biome": "2.3.11" + "@biomejs/biome": "2.3.11", + "vitest": "^3.0.0" } } diff --git a/packages/skills-build/src/build.ts b/packages/skills-build/src/build.ts index bb9561d..88afd33 100644 --- a/packages/skills-build/src/build.ts +++ b/packages/skills-build/src/build.ts @@ -8,7 +8,7 @@ import { unlinkSync, writeFileSync, } from "node:fs"; -import { basename, join } from "node:path"; +import { basename, join, relative } from "node:path"; import { discoverSkills, getSkillPaths, @@ -345,8 +345,10 @@ function buildSkill(paths: SkillPaths): void { for (const file of referenceFiles) { const name = basename(file, ".md"); const prefix = name.split("-")[0]; + // Compute relative path from references directory + const relativePath = relative(paths.referencesDir, file); const group = grouped.get(prefix) || []; - group.push(name); + group.push(relativePath); grouped.set(prefix, group); } @@ -355,7 +357,7 @@ function buildSkill(paths: SkillPaths): void { const title = section ? section.title : prefix; output.push(`**${title}** (\`${prefix}-\`):`); for (const file of files.sort()) { - output.push(`- \`references/${file}.md\``); + output.push(`- \`references/${file}\``); } output.push(""); } diff --git a/skills/supabase/AGENTS.md b/skills/supabase/AGENTS.md new file mode 100644 index 0000000..4f7832e --- /dev/null +++ b/skills/supabase/AGENTS.md @@ -0,0 +1,72 @@ +# supabase + +> **Note:** `CLAUDE.md` is a symlink to this file. + +## Overview + +Guides and best practices for working with Supabase. Covers getting started, Auth, Database, Storage, Edge Functions, Realtime, supabase-js SDK, CLI, and MCP integration. Use for any Supabase-related questions. + +## Structure + +``` +supabase/ + SKILL.md # Main skill file - read this first + AGENTS.md # This navigation guide + CLAUDE.md # Symlink to AGENTS.md + references/ # Detailed reference files +``` + +## Usage + +1. Read `SKILL.md` for the main skill instructions +2. Browse `references/` for detailed documentation on specific topics +3. Reference files are loaded on-demand - read only what you need + +## Reference Categories + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Row Level Security | CRITICAL | `rls-` | +| 2 | Connection Pooling | CRITICAL | `conn-` | +| 3 | Schema Design | HIGH | `schema-` | +| 4 | Migrations | HIGH | `migrations-` | +| 5 | Performance | CRITICAL | `perf-` | +| 6 | Security | CRITICAL | `security-` | + +Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`). + +## Available References + +**Connection Pooling** (`conn-`): +- `references/db/conn-pooling.md` + +**Migrations** (`migrations-`): +- `references/db/migrations-diff.md` +- `references/db/migrations-idempotent.md` +- `references/db/migrations-testing.md` + +**Performance** (`perf-`): +- `references/db/perf-indexes.md` +- `references/db/perf-query-optimization.md` + +**Row Level Security** (`rls-`): +- `references/db/rls-common-mistakes.md` +- `references/db/rls-mandatory.md` +- `references/db/rls-performance.md` +- `references/db/rls-policy-types.md` +- `references/db/rls-views.md` + +**Schema Design** (`schema-`): +- `references/db/schema-auth-fk.md` +- `references/db/schema-extensions.md` +- `references/db/schema-jsonb.md` +- `references/db/schema-realtime.md` +- `references/db/schema-timestamps.md` + +**Security** (`security-`): +- `references/db/security-functions.md` +- `references/db/security-service-role.md` + +--- + +*18 reference files across 6 categories* \ No newline at end of file diff --git a/skills/supabase/CLAUDE.md b/skills/supabase/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/skills/supabase/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/skills/supabase/SKILL.md b/skills/supabase/SKILL.md new file mode 100644 index 0000000..bf4ec48 --- /dev/null +++ b/skills/supabase/SKILL.md @@ -0,0 +1,84 @@ +--- +name: supabase +description: Guides and best practices for working with Supabase. Covers getting started, Auth, Database, Storage, Edge Functions, Realtime, supabase-js SDK, CLI, and MCP integration. Use for any Supabase-related questions. +license: MIT +metadata: + author: supabase + version: "1.0.0" + organization: Supabase + date: January 2026 + abstract: Comprehensive Supabase development guide for building applications with Supabase services. Contains guides covering Auth, Database, Storage, Edge Functions, Realtime, client libraries, CLI, and tooling. Each reference includes setup instructions, code examples, common mistakes, and integration patterns. +--- + +# Supabase + +Supabase is an open source Firebase alternative that provides a Postgres database, authentication, instant APIs, edge functions, realtime subscriptions, and storage. It's fully compatible with Postgres and works with any language, framework, or ORM. + +## Supabase Documentation + +Always reference the Supabase documentation before making Supabase-related claims. The documentation is the source of truth for all Supabase-related information. + +You can use the `curl` commands to fetch the documentation page as markdown: + +**Documentation:** + +```bash +# Fetch any doc page as markdown +curl -H "Accept: text/markdown" https://supabase.com/docs/ +``` + +## Overview of Resources + +Reference the appropriate resource file based on the user's needs: + +### Core Guides + +| Area | Resource | When to Use | +| ---------------- | -------------------------------- | -------------------------------------------------------- | +| Getting Started | `references/getting-started.md` | Setting up a project, connection strings, dependencies | +| Referencing Docs | `references/referencing-docs.md` | Looking up official documentation, verifying information | + +### Authentication & Security + +| Area | Resource | When to Use | +| ------------------ | -------------------- | ------------------------------------------ | +| Auth Overview | `references/auth.md` | Authentication, social login, sessions | +| Row Level Security | `references/rls.md` | Database security policies, access control | + +### Database + +| Area | Resource | When to Use | +| ------------------ | ------------------------------- | ---------------------------------------------- | +| Database | `references/database.md` | Postgres queries, migrations, modeling | +| RLS Security | `references/db/rls-*.md` | Row Level Security policies, common mistakes | +| Connection Pooling | `references/db/conn-pooling.md` | Transaction vs Session mode, port 6543 vs 5432 | +| Schema Design | `references/db/schema-*.md` | auth.users FKs, timestamps, JSONB, extensions | +| Migrations | `references/db/migrations-*.md` | CLI workflows, idempotent patterns, db diff | +| Performance | `references/db/perf-*.md` | Indexes (BRIN, GIN), query optimization | +| Security | `references/db/security-*.md` | Service role key, security_definer functions | + +### Storage & Media + +| Area | Resource | When to Use | +| ------- | ----------------------- | ---------------------------- | +| Storage | `references/storage.md` | File uploads, buckets, media | + +### Edge Functions + +| Area | Resource | When to Use | +| -------------- | ------------------------------ | -------------------------------------------- | +| Edge Functions | `references/edge-functions.md` | Serverless functions, Deno runtime, webhooks | + +### Realtime + +| Area | Resource | When to Use | +| -------- | ------------------------ | -------------------------------------------- | +| Realtime | `references/realtime.md` | Real-time subscriptions, presence, broadcast | + +### Client Libraries & CLI + +| Area | Resource | When to Use | +| ------------ | --------------------------- | ---------------------------------------- | +| supabase-js | `references/supabase-js.md` | JavaScript/TypeScript SDK, client config | +| Supabase CLI | `references/cli.md` | Local development, migrations, CI/CD | +| MCP Server | `references/mcp.md` | AI agent integration, MCP tooling | diff --git a/skills/supabase/references/_template.md b/skills/supabase/references/_template.md new file mode 100644 index 0000000..e10af58 --- /dev/null +++ b/skills/supabase/references/_template.md @@ -0,0 +1,46 @@ +--- +title: Action-Oriented Title +tags: relevant, keywords +--- + +# Feature Name + +One-sentence description of what this does and when to use it. + +## Quick Start + +```typescript +// Minimal working example with real code +import { createClient } from "@supabase/supabase-js"; +const supabase = createClient(url, key); + +// Core operation +const { data, error } = await supabase.from("table").select("*"); +``` + +## Common Patterns + +### Pattern Name + +```typescript +// Concrete example - prefer this over explanations +const { data } = await supabase.from("users").select("id, email").eq("active", true); +``` + +## Common Mistakes + +**Mistake**: Brief description of what goes wrong. + +```typescript +// Incorrect +const data = await supabase.from("users").select(); // Missing error handling + +// Correct +const { data, error } = await supabase.from("users").select("*"); +if (error) throw error; +``` + +## Related + +- [subtopic.md](subtopic.md) - For advanced X patterns +- [Docs](https://supabase.com/docs/guides/feature) - Official guide diff --git a/skills/supabase/references/db/_sections.md b/skills/supabase/references/db/_sections.md new file mode 100644 index 0000000..abebb28 --- /dev/null +++ b/skills/supabase/references/db/_sections.md @@ -0,0 +1,36 @@ +# Section Definitions + +Reference files are grouped by prefix. Claude loads specific files based on user +queries. + +--- + +## 1. Row Level Security (rls) + +**Impact:** CRITICAL +**Description:** RLS policies, common mistakes, performance optimizations, and security patterns specific to Supabase's auth.uid() integration. + +## 2. Connection Pooling (conn) + +**Impact:** CRITICAL +**Description:** Supabase-specific connection pooling with Supavisor. Transaction mode (port 6543) vs Session mode (port 5432). + +## 3. Schema Design (schema) + +**Impact:** HIGH +**Description:** Supabase-specific schema patterns including auth.users foreign keys, timestamptz, JSONB usage, extensions, and Realtime. + +## 4. Migrations (migrations) + +**Impact:** HIGH +**Description:** Migration workflows using Supabase CLI, idempotent patterns, supabase db diff, and local testing strategies. + +## 5. Performance (perf) + +**Impact:** CRITICAL +**Description:** Index strategies (BRIN, GIN, partial), query optimization for PostgREST, and Supabase-specific performance patterns. + +## 6. Security (security) + +**Impact:** CRITICAL +**Description:** Service role key handling, security definer functions in private schemas, and Supabase-specific security patterns. diff --git a/skills/supabase/references/db/conn-pooling.md b/skills/supabase/references/db/conn-pooling.md new file mode 100644 index 0000000..9e5521b --- /dev/null +++ b/skills/supabase/references/db/conn-pooling.md @@ -0,0 +1,106 @@ +--- +title: Use Correct Connection Pooling Mode +impact: CRITICAL +impactDescription: Prevents connection exhaustion and enables 10-100x scalability +tags: connection-pooling, supavisor, transaction-mode, session-mode +--- + +## Use Correct Connection Pooling Mode + +Supabase provides Supavisor for connection pooling. Choose the right mode based +on your application type. + +## Transaction Mode (Port 6543) + +Best for: Serverless functions, edge computing, stateless APIs. + +```bash +## Transaction mode connection string +postgres://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres +``` + +**Limitations:** + +- No prepared statements +- No SET commands +- No LISTEN/NOTIFY +- No temp tables + +```javascript +// Prisma - disable prepared statements +const prisma = new PrismaClient({ + datasources: { + db: { + url: process.env.DATABASE_URL + "?pgbouncer=true", + }, + }, +}); +``` + +## Session Mode (Port 5432) + +Best for: Long-running servers, apps needing prepared statements. + +```bash +## Session mode (via pooler for IPv4) +postgres://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:5432/postgres +``` + +## Direct Connection (Port 5432) + +Best for: Migrations, admin tasks, persistent servers. + +```bash +## Direct connection (IPv6 only unless IPv4 add-on enabled) +postgres://postgres.[ref]:[password]@db.[ref].supabase.co:5432/postgres +``` + +## Common Mistakes + +**Incorrect:** + +```javascript +// Serverless with session mode - exhausts connections +const pool = new Pool({ + connectionString: "...pooler.supabase.com:5432/postgres", + max: 20, // Too many connections per instance! +}); +``` + +**Correct:** + +```javascript +// Serverless with transaction mode +const pool = new Pool({ + connectionString: "...pooler.supabase.com:6543/postgres", + max: 1, // Single connection per serverless instance +}); +``` + +**Incorrect:** + +```bash +## Transaction mode with prepared statements +DATABASE_URL="...pooler.supabase.com:6543/postgres" +## Error: prepared statement already exists +``` + +**Correct:** + +```bash +## Add pgbouncer=true to disable prepared statements +DATABASE_URL="...pooler.supabase.com:6543/postgres?pgbouncer=true" +``` + +## Connection Limits by Compute Size + +| Compute | Direct Connections | Pooler Clients | +| ------- | ------------------ | -------------- | +| Nano | 60 | 200 | +| Small | 90 | 400 | +| Medium | 120 | 600 | +| Large | 160 | 800 | + +## Related + +- [Docs](https://supabase.com/docs/guides/database/connecting-to-postgres) diff --git a/skills/supabase/references/db/migrations-diff.md b/skills/supabase/references/db/migrations-diff.md new file mode 100644 index 0000000..ed6e979 --- /dev/null +++ b/skills/supabase/references/db/migrations-diff.md @@ -0,0 +1,97 @@ +--- +title: Use supabase db diff for Dashboard Changes +impact: HIGH +impactDescription: Captures manual changes into version-controlled migrations +tags: migrations, supabase-cli, db-diff, dashboard +--- + +## Use supabase db diff for Dashboard Changes + +When making schema changes via Dashboard, use `supabase db diff` to generate +migration files for version control. + +**Incorrect:** + +```sql +-- Making changes in Dashboard without capturing them +-- Changes exist in remote but not in version control +-- Team members can't reproduce the database state +``` + +**Correct:** + +```bash +# After making Dashboard changes, generate migration +supabase db diff -f add_profiles_table + +# Review and test +supabase db reset + +# Commit to version control +git add supabase/migrations/ +git commit -m "Add profiles table migration" +``` + +## Workflow + +1. Make changes in Supabase Dashboard (create tables, add columns, etc.) +2. Generate migration from diff: + +```bash +supabase db diff -f add_profiles_table +``` + +3. Review generated migration in `supabase/migrations/` +4. Test locally: + +```bash +supabase db reset +``` + +5. Commit migration to version control + +## Diff Against Local Database + +```bash +# Start local Supabase +supabase start + +# Make changes via Dashboard or SQL +# Generate diff +supabase db diff -f my_changes +``` + +## Diff Against Remote Database + +```bash +# Link to remote project +supabase link --project-ref your-project-ref + +# Pull remote schema and generate diff +supabase db diff --linked -f sync_remote_changes +``` + +## What diff Captures + +- Tables and columns +- Indexes +- Constraints +- Functions and triggers +- RLS policies +- Extensions + +## What diff Does NOT Capture + +- DML (INSERT, UPDATE, DELETE) +- View ownership changes +- Materialized views +- Partitions +- Comments + +For these, write manual migrations. + +## Related + +- [migrations-idempotent.md](migrations-idempotent.md) +- [migrations-testing.md](migrations-testing.md) +- [Docs](https://supabase.com/docs/guides/deployment/database-migrations) diff --git a/skills/supabase/references/db/migrations-idempotent.md b/skills/supabase/references/db/migrations-idempotent.md new file mode 100644 index 0000000..8f52cbe --- /dev/null +++ b/skills/supabase/references/db/migrations-idempotent.md @@ -0,0 +1,90 @@ +--- +title: Write Idempotent Migrations +impact: HIGH +impactDescription: Safe to run multiple times, prevents migration failures +tags: migrations, idempotent, supabase-cli +--- + +## Write Idempotent Migrations + +Migrations should be safe to run multiple times without errors. Use +`IF NOT EXISTS` and `IF EXISTS` clauses. + +**Incorrect:** + +```sql +-- Fails on second run: "relation already exists" +create table users ( + id uuid primary key, + email text not null +); + +create index idx_users_email on users(email); +``` + +**Correct:** + +```sql +-- Safe to run multiple times +create table if not exists users ( + id uuid primary key, + email text not null +); + +create index if not exists idx_users_email on users(email); +``` + +## Idempotent Column Additions + +```sql +-- Add column only if it doesn't exist +do $$ +begin + if not exists ( + select 1 from information_schema.columns + where table_name = 'users' and column_name = 'phone' + ) then + alter table users add column phone text; + end if; +end $$; +``` + +## Idempotent Drops + +```sql +-- Safe drops +drop table if exists old_table; +drop index if exists old_index; +drop function if exists old_function(); +``` + +## Idempotent Policies + +```sql +-- Drop and recreate to update policy +drop policy if exists "Users see own data" on users; + +create policy "Users see own data" on users + for select to authenticated + using ((select auth.uid()) = id); +``` + +## Migration File Naming + +Migrations in `supabase/migrations/` are named with timestamps: + +``` +20240315120000_create_users.sql +20240315130000_add_profiles.sql +``` + +Create new migration: + +```bash +supabase migration new create_users +``` + +## Related + +- [migrations-testing.md](migrations-testing.md) +- [Docs](https://supabase.com/docs/guides/deployment/database-migrations) diff --git a/skills/supabase/references/db/migrations-testing.md b/skills/supabase/references/db/migrations-testing.md new file mode 100644 index 0000000..fd8f9d3 --- /dev/null +++ b/skills/supabase/references/db/migrations-testing.md @@ -0,0 +1,116 @@ +--- +title: Test Migrations with supabase db reset +impact: MEDIUM-HIGH +impactDescription: Catch migration errors before production deployment +tags: migrations, testing, supabase-cli, local-development +--- + +## Test Migrations with supabase db reset + +Always test migrations locally before deploying to production. Use +`supabase db reset` to verify migrations run cleanly from scratch. + +**Incorrect:** + +```bash +# Deploying directly without testing +supabase db push # Migration fails in production! +``` + +**Correct:** + +```bash +# Test migrations locally first +supabase db reset # Runs all migrations from scratch + +# Verify success, then deploy +supabase db push +``` + +## Testing Workflow + +```bash +# Start local Supabase +supabase start + +# Reset database and run all migrations +supabase db reset + +# Verify tables and data +supabase inspect db table-sizes +``` + +## What db reset Does + +1. Drops the local database +2. Creates a fresh database +3. Runs all migrations in order +4. Runs `supabase/seed.sql` if present + +## Seed Data for Testing + +Create `supabase/seed.sql` for test data: + +```sql +-- supabase/seed.sql +-- Runs after migrations on db reset + +-- Use ON CONFLICT for idempotency +insert into categories (name) +values ('Action'), ('Comedy'), ('Drama') +on conflict (name) do nothing; + +-- Test users (only in local development!) +insert into profiles (id, username) +values ('00000000-0000-0000-0000-000000000001', 'testuser') +on conflict (id) do nothing; +``` + +## Test Specific Migration + +```bash +# Apply single pending migration +supabase migration up + +# Check migration status +supabase migration list +``` + +## Repair Failed Migration + +If a migration partially fails: + +```bash +# Fix the migration file +# Then repair the migration history +supabase migration repair --status applied 20240315120000 +``` + +## Inspect Database State + +```bash +# View tables +supabase inspect db table-sizes + +# View indexes +supabase inspect db index-usage + +# View cache hit rate +supabase inspect db cache-hit +``` + +## CI/CD Integration + +```yaml +# GitHub Actions example +- name: Test migrations + run: | + supabase start + supabase db reset + supabase test db # Run pgTAP tests +``` + +## Related + +- [migrations-idempotent.md](migrations-idempotent.md) +- [Docs](https://supabase.com/docs/guides/local-development/overview) diff --git a/skills/supabase/references/db/perf-indexes.md b/skills/supabase/references/db/perf-indexes.md new file mode 100644 index 0000000..6cb750c --- /dev/null +++ b/skills/supabase/references/db/perf-indexes.md @@ -0,0 +1,114 @@ +--- +title: Choose the Right Index Type +impact: CRITICAL +impactDescription: 10-1000x query performance improvements with proper indexing +tags: indexes, performance, btree, brin, gin, partial +--- + +## Choose the Right Index Type + +Supabase uses PostgreSQL indexes. Choose the right type for your query patterns. + +## B-Tree (Default) + +Best for: Equality, range queries, sorting. + +```sql +-- Equality and range queries +create index idx_users_email on users(email); +create index idx_orders_created on orders(created_at); + +-- Composite index for multi-column queries +create index idx_orders_user_status on orders(user_id, status); +``` + +## BRIN (Block Range Index) + +Best for: Large tables with naturally ordered data (timestamps, sequential IDs). +10x+ smaller than B-tree. + +```sql +-- Perfect for append-only timestamp columns +create index idx_logs_created on logs using brin(created_at); +create index idx_events_id on events using brin(id); +``` + +**When to use:** Tables with millions of rows where data is inserted in order. + +## GIN (Generalized Inverted Index) + +Best for: JSONB, arrays, full-text search. + +```sql +-- JSONB containment queries +create index idx_users_metadata on users using gin(metadata); + +-- Full-text search +create index idx_posts_search on posts using gin(to_tsvector('english', title || ' ' || content)); + +-- Array containment +create index idx_tags on posts using gin(tags); +``` + +## Partial Index + +Best for: Queries that filter on specific values. + +```sql +-- Only index active users (smaller, faster) +create index idx_active_users on users(email) +where status = 'active'; + +-- Only index unprocessed orders +create index idx_pending_orders on orders(created_at) +where processed = false; +``` + +**Requirement:** Query WHERE clause must match index condition. + +## Common Mistakes + +**Incorrect:** + +```sql +-- Over-indexing: slows writes, wastes space +create index idx_users_1 on users(email); +create index idx_users_2 on users(email, name); +create index idx_users_3 on users(name, email); +create index idx_users_4 on users(name); +``` + +**Correct:** + +```sql +-- Minimal indexes based on actual queries +create index idx_users_email on users(email); -- For login +create index idx_users_name on users(name); -- For search +``` + +## Verify Index Usage + +```sql +-- Check if query uses index +explain analyze +select * from users where email = 'test@example.com'; + +-- Find unused indexes +select * from pg_stat_user_indexes +where idx_scan = 0 and indexrelname not like '%_pkey'; +``` + +## Concurrently Create Indexes + +For production tables, avoid locking: + +```sql +-- Doesn't block writes +create index concurrently idx_users_email on users(email); +``` + +## Related + +- [rls-performance.md](rls-performance.md) +- [schema-jsonb.md](schema-jsonb.md) +- [Docs](https://supabase.com/docs/guides/database/postgres/indexes) diff --git a/skills/supabase/references/db/perf-query-optimization.md b/skills/supabase/references/db/perf-query-optimization.md new file mode 100644 index 0000000..ce7b2e0 --- /dev/null +++ b/skills/supabase/references/db/perf-query-optimization.md @@ -0,0 +1,149 @@ +--- +title: Optimize Queries for PostgREST +impact: HIGH +impactDescription: Faster API responses and reduced database load +tags: postgrest, queries, performance, optimization, supabase-js +--- + +## Optimize Queries for PostgREST + +Supabase uses PostgREST to generate REST APIs. Optimize queries for better +performance. + +## Select Only Needed Columns + +**Incorrect:** + +```javascript +// Fetches all columns including large text/blobs +const { data } = await supabase.from("posts").select("*"); +``` + +**Correct:** + +```javascript +// Only fetch needed columns +const { data } = await supabase.from("posts").select("id, title, author_id"); +``` + +## Use Explicit Filters + +Explicit filters help the query planner, even with RLS. + +**Incorrect:** + +```javascript +// Relies only on RLS - query planner has less info +const { data } = await supabase.from("posts").select("*"); +``` + +**Correct:** + +```javascript +// Explicit filter improves query plan +const { data } = await supabase + .from("posts") + .select("*") + .eq("author_id", userId); +``` + +## Always Paginate + +**Incorrect:** + +```javascript +// Could return thousands of rows +const { data } = await supabase.from("posts").select("*"); +``` + +**Correct:** + +```javascript +// Paginate results +const { data } = await supabase + .from("posts") + .select("*") + .range(0, 19) // First 20 rows + .order("created_at", { ascending: false }); +``` + +## Efficient Joins + +**Incorrect:** + +```javascript +// N+1: One query per post for author +const { data: posts } = await supabase.from("posts").select("*"); +for (const post of posts) { + const { data: author } = await supabase + .from("users") + .select("*") + .eq("id", post.author_id) + .single(); +} +``` + +**Correct:** + +```javascript +// Single query with embedded join +const { data } = await supabase.from("posts").select(` + id, + title, + author:users ( + id, + name, + avatar_url + ) + `); +``` + +## Use count Option Efficiently + +**Incorrect:** + +```javascript +// Counts ALL rows (slow on large tables) +const { count } = await supabase + .from("posts") + .select("*", { count: "exact", head: true }); +``` + +**Correct:** + +```javascript +// Estimated count (fast) +const { count } = await supabase + .from("posts") + .select("*", { count: "estimated", head: true }); + +// Or planned count (uses query planner estimate) +const { count } = await supabase + .from("posts") + .select("*", { count: "planned", head: true }); +``` + +## Debug Query Performance + +```javascript +// Get query execution plan +const { data } = await supabase + .from("posts") + .select("*") + .eq("author_id", userId) + .explain({ analyze: true, verbose: true }); + +console.log(data); // Shows execution plan +``` + +Enable explain in database: + +```sql +alter role authenticator set pgrst.db_plan_enabled to true; +notify pgrst, 'reload config'; +``` + +## Related + +- [perf-indexes.md](perf-indexes.md) +- [Docs](https://supabase.com/docs/guides/database/query-optimization) diff --git a/skills/supabase/references/db/rls-common-mistakes.md b/skills/supabase/references/db/rls-common-mistakes.md new file mode 100644 index 0000000..0434bae --- /dev/null +++ b/skills/supabase/references/db/rls-common-mistakes.md @@ -0,0 +1,97 @@ +--- +title: Avoid Common RLS Policy Mistakes +impact: CRITICAL +impactDescription: Prevents security vulnerabilities and unintended data exposure +tags: rls, security, auth.uid, policies, common-mistakes +--- + +## Avoid Common RLS Policy Mistakes + +## 1. Missing TO Clause + +Without `TO`, policies apply to all roles including `anon`. + +**Incorrect:** + +```sql +-- Runs for both anon and authenticated users +create policy "Users see own data" on profiles + using (auth.uid() = user_id); +``` + +**Correct:** + +```sql +-- Only runs for authenticated users +create policy "Users see own data" on profiles + to authenticated + using (auth.uid() = user_id); +``` + +## 2. Using user_metadata for Authorization + +Users can modify their own `user_metadata`. Use `app_metadata` instead. + +**Incorrect:** + +```sql +-- DANGEROUS: users can set their own role! +using ((auth.jwt() -> 'user_metadata' ->> 'role') = 'admin') +``` + +**Correct:** + +```sql +-- app_metadata cannot be modified by users +using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin') +``` + +## 3. Not Checking NULL auth.uid() + +For unauthenticated users, `auth.uid()` returns NULL. + +**Incorrect:** + +```sql +-- NULL = NULL is NULL (not true), but confusing behavior +using (auth.uid() = user_id) +``` + +**Correct:** + +```sql +-- Explicit NULL check +using (auth.uid() is not null and auth.uid() = user_id) +``` + +## 4. Missing SELECT Policy for UPDATE + +UPDATE operations require a SELECT policy to find rows to update. + +**Incorrect:** + +```sql +-- UPDATE silently fails - no rows found +create policy "Users can update" on profiles + for update to authenticated + using (auth.uid() = user_id); +``` + +**Correct:** + +```sql +-- Need both SELECT and UPDATE policies +create policy "Users can view" on profiles + for select to authenticated + using (auth.uid() = user_id); + +create policy "Users can update" on profiles + for update to authenticated + using (auth.uid() = user_id) + with check (auth.uid() = user_id); +``` + +## Related + +- [rls-mandatory.md](rls-mandatory.md) +- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security) diff --git a/skills/supabase/references/db/rls-mandatory.md b/skills/supabase/references/db/rls-mandatory.md new file mode 100644 index 0000000..1308837 --- /dev/null +++ b/skills/supabase/references/db/rls-mandatory.md @@ -0,0 +1,50 @@ +--- +title: Enable RLS on All Exposed Schemas +impact: CRITICAL +impactDescription: Prevents unauthorized data access at the database level +tags: rls, security, auth, policies +--- + +## Enable RLS on All Exposed Schemas + +RLS must be enabled on every table in exposed schemas (default: `public`). Without +RLS, any user with the anon key can read and write all data. + +**Incorrect:** + +```sql +-- Table without RLS - anyone can read/write everything +create table profiles ( + id uuid primary key, + user_id uuid, + bio text +); +``` + +**Correct:** + +```sql +create table profiles ( + id uuid primary key, + user_id uuid references auth.users(id) on delete cascade, + bio text +); + +-- Enable RLS +alter table profiles enable row level security; + +-- Create policy +create policy "Users can view own profile" + on profiles for select + to authenticated + using (auth.uid() = user_id); +``` + +Tables created via Dashboard have RLS enabled by default. Tables created via SQL +require manual enablement. Supabase sends daily warnings for tables without RLS. + +**Note:** Service role key bypasses ALL RLS policies. Never expose it to browsers. + +## Related + +- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security) diff --git a/skills/supabase/references/db/rls-performance.md b/skills/supabase/references/db/rls-performance.md new file mode 100644 index 0000000..c559e53 --- /dev/null +++ b/skills/supabase/references/db/rls-performance.md @@ -0,0 +1,108 @@ +--- +title: Optimize RLS Policy Performance +impact: CRITICAL +impactDescription: Achieve 100x-99,000x query performance improvements +tags: rls, performance, optimization, indexes, auth.uid +--- + +## Optimize RLS Policy Performance + +RLS policies run on every row access. Unoptimized policies cause severe +performance degradation. + +## 1. Wrap auth.uid() in SELECT (94-99% improvement) + +**Incorrect:** + +```sql +-- auth.uid() called for every row +create policy "Users see own data" on profiles + to authenticated + using (auth.uid() = user_id); +``` + +**Correct:** + +```sql +-- Cached once per statement via initPlan +create policy "Users see own data" on profiles + to authenticated + using ((select auth.uid()) = user_id); +``` + +## 2. Add Indexes on Policy Columns (99% improvement) + +**Incorrect:** + +```sql +-- Full table scan for every query +create policy "Users see own data" on profiles + using ((select auth.uid()) = user_id); +-- No index on user_id +``` + +**Correct:** + +```sql +create policy "Users see own data" on profiles + using ((select auth.uid()) = user_id); + +-- Add index on filtered column +create index idx_profiles_user_id on profiles(user_id); +``` + +## 3. Use Explicit Filters in Queries (94% improvement) + +**Incorrect:** + +```javascript +// Relies only on implicit RLS filter +const { data } = await supabase.from("profiles").select("*"); +``` + +**Correct:** + +```javascript +// Add explicit filter - helps query planner +const { data } = await supabase + .from("profiles") + .select("*") + .eq("user_id", userId); +``` + +## 4. Use Security Definer Functions for Joins + +**Incorrect:** + +```sql +-- Join in policy - executed per row +using ( + user_id in ( + select user_id from team_members + where team_id = teams.id -- joins! + ) +) +``` + +**Correct:** + +```sql +-- Function in private schema +create function private.user_team_ids() +returns setof uuid +language sql +security definer +stable +as $$ + select team_id from team_members + where user_id = (select auth.uid()) +$$; + +-- Policy uses cached function result +using (team_id in (select private.user_team_ids())) +``` + +## Related + +- [security-functions.md](security-functions.md) +- [Supabase RLS Performance Guide](https://github.com/orgs/supabase/discussions/14576) diff --git a/skills/supabase/references/db/rls-policy-types.md b/skills/supabase/references/db/rls-policy-types.md new file mode 100644 index 0000000..bd45747 --- /dev/null +++ b/skills/supabase/references/db/rls-policy-types.md @@ -0,0 +1,81 @@ +--- +title: Use RESTRICTIVE vs PERMISSIVE Policies +impact: MEDIUM-HIGH +impactDescription: Controls policy combination logic to prevent unintended access +tags: rls, policies, permissive, restrictive +--- + +## Use RESTRICTIVE vs PERMISSIVE Policies + +Supabase RLS supports two policy types with different combination logic. + +## PERMISSIVE (Default) + +Multiple permissive policies combine with OR logic. If ANY policy passes, access +is granted. + +```sql +-- User can access if they own it OR are an admin +create policy "Owner access" on documents + for select to authenticated + using (owner_id = (select auth.uid())); + +create policy "Admin access" on documents + for select to authenticated + using ((select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'); +``` + +## RESTRICTIVE + +Restrictive policies combine with AND logic. ALL restrictive policies must pass. + +**Use Case: Enforce MFA for sensitive operations** + +```sql +-- Base access policy (permissive) +create policy "Users can view own data" on sensitive_data + for select to authenticated + using (user_id = (select auth.uid())); + +-- MFA requirement (restrictive) - MUST also pass +create policy "Require MFA" on sensitive_data + as restrictive + for select to authenticated + using ((select auth.jwt() ->> 'aal') = 'aal2'); +``` + +**Use Case: Block OAuth client access** + +```sql +-- Allow direct session access +create policy "Direct access only" on payment_methods + as restrictive + for all to authenticated + using ((select auth.jwt() ->> 'client_id') is null); +``` + +## Common Mistake + +**Incorrect:** + +```sql +-- Intended as additional requirement, but PERMISSIVE means OR +create policy "Require MFA" on sensitive_data + for select to authenticated + using ((select auth.jwt() ->> 'aal') = 'aal2'); +``` + +**Correct:** + +```sql +-- AS RESTRICTIVE makes it an AND requirement +create policy "Require MFA" on sensitive_data + as restrictive + for select to authenticated + using ((select auth.jwt() ->> 'aal') = 'aal2'); +``` + +## Related + +- [rls-common-mistakes.md](rls-common-mistakes.md) +- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security) diff --git a/skills/supabase/references/db/rls-views.md b/skills/supabase/references/db/rls-views.md new file mode 100644 index 0000000..7aa5d7e --- /dev/null +++ b/skills/supabase/references/db/rls-views.md @@ -0,0 +1,65 @@ +--- +title: Use security_invoker for Views with RLS +impact: HIGH +impactDescription: Ensures views respect RLS policies instead of bypassing them +tags: rls, views, security_invoker, security +--- + +## Use security_invoker for Views with RLS + +By default, views run as the view owner (security definer), bypassing RLS on +underlying tables. + +**Incorrect:** + +```sql +-- View bypasses RLS - exposes all data! +create view public_profiles as + select id, username, avatar_url + from profiles; +``` + +**Correct (Postgres 15+):** + +```sql +-- View respects RLS of querying user +create view public_profiles +with (security_invoker = true) +as + select id, username, avatar_url + from profiles; +``` + +**Correct (Older Postgres):** + +```sql +-- Option 1: Revoke direct access, create RLS on view +revoke all on public_profiles from anon, authenticated; + +-- Option 2: Create view in unexposed schema +create schema private; +create view private.profiles_view as + select * from profiles; +``` + +## When to Use security_definer + +Use `security_definer = true` (default) when the view intentionally aggregates +or filters data that users shouldn't access directly: + +```sql +-- Intentionally exposes limited public data +create view leaderboard as + select username, score + from profiles + order by score desc + limit 100; + +-- Grant read access +grant select on leaderboard to anon; +``` + +## Related + +- [rls-mandatory.md](rls-mandatory.md) +- [Docs](https://supabase.com/docs/guides/database/postgres/row-level-security) diff --git a/skills/supabase/references/db/schema-auth-fk.md b/skills/supabase/references/db/schema-auth-fk.md new file mode 100644 index 0000000..cb0609a --- /dev/null +++ b/skills/supabase/references/db/schema-auth-fk.md @@ -0,0 +1,80 @@ +--- +title: Add CASCADE to auth.users Foreign Keys +impact: HIGH +impactDescription: Prevents orphaned records and user deletion failures +tags: foreign-keys, auth.users, cascade, schema-design +--- + +## Add CASCADE to auth.users Foreign Keys + +When referencing `auth.users`, always specify `ON DELETE CASCADE`. Without it, +deleting users fails with foreign key violations. + +**Incorrect:** + +```sql +-- User deletion fails: "foreign key violation" +create table profiles ( + id uuid primary key references auth.users(id), + username text, + avatar_url text +); +``` + +**Correct:** + +```sql +-- Profile deleted automatically when user is deleted +create table profiles ( + id uuid primary key references auth.users(id) on delete cascade, + username text, + avatar_url text +); +``` + +## Alternative: SET NULL for Optional Relationships + +Use `ON DELETE SET NULL` when the record should persist without the user: + +```sql +create table comments ( + id bigint primary key generated always as identity, + author_id uuid references auth.users(id) on delete set null, + content text not null, + created_at timestamptz default now() +); +-- Comment remains with author_id = NULL after user deletion +``` + +## Auto-Create Profile on Signup + +```sql +create or replace function public.handle_new_user() +returns trigger +language plpgsql +security definer +set search_path = '' +as $$ +begin + insert into public.profiles (id, email, full_name) + values ( + new.id, + new.email, + new.raw_user_meta_data ->> 'full_name' + ); + return new; +end; +$$; + +create trigger on_auth_user_created + after insert on auth.users + for each row execute function public.handle_new_user(); +``` + +**Important:** Use `security definer` and `set search_path = ''` for triggers on +auth.users. + +## Related + +- [security-functions.md](security-functions.md) +- [Docs](https://supabase.com/docs/guides/database/postgres/cascade-deletes) diff --git a/skills/supabase/references/db/schema-extensions.md b/skills/supabase/references/db/schema-extensions.md new file mode 100644 index 0000000..7fc4913 --- /dev/null +++ b/skills/supabase/references/db/schema-extensions.md @@ -0,0 +1,80 @@ +--- +title: Install Extensions in extensions Schema +impact: MEDIUM +impactDescription: Keeps public schema clean and simplifies migrations +tags: extensions, schema-design, best-practices +--- + +## Install Extensions in extensions Schema + +Install PostgreSQL extensions in the `extensions` schema to keep the `public` +schema clean and avoid conflicts with application tables. + +**Incorrect:** + +```sql +-- Installs in public schema by default +create extension pg_trgm; +create extension pgvector; +``` + +**Correct:** + +```sql +-- Install in extensions schema +create extension if not exists pg_trgm with schema extensions; +create extension if not exists vector with schema extensions; + +-- Reference with schema prefix +create index idx_name_trgm on users + using gin(name extensions.gin_trgm_ops); +``` + +## Common Supabase Extensions + +```sql +-- Vector similarity search (AI embeddings) +create extension if not exists vector with schema extensions; + +-- Scheduled jobs +create extension if not exists pg_cron with schema extensions; + +-- HTTP requests from database +create extension if not exists pg_net with schema extensions; + +-- Full-text search improvements +create extension if not exists pg_trgm with schema extensions; + +-- Geospatial data +create extension if not exists postgis with schema extensions; + +-- UUID generation (enabled by default) +create extension if not exists "uuid-ossp" with schema extensions; +``` + +## Check Available Extensions + +```sql +-- List available extensions +select * from pg_available_extensions; + +-- List installed extensions +select * from pg_extension; +``` + +## Using Extensions + +```sql +-- pgvector example +create table documents ( + id bigint primary key generated always as identity, + content text, + embedding vector(1536) -- OpenAI ada-002 dimensions +); + +create index on documents using ivfflat (embedding vector_cosine_ops); +``` + +## Related + +- [Docs](https://supabase.com/docs/guides/database/extensions) diff --git a/skills/supabase/references/db/schema-jsonb.md b/skills/supabase/references/db/schema-jsonb.md new file mode 100644 index 0000000..1a36b6f --- /dev/null +++ b/skills/supabase/references/db/schema-jsonb.md @@ -0,0 +1,95 @@ +--- +title: Use Structured Columns Over JSONB When Possible +impact: MEDIUM +impactDescription: Improves query performance, type safety, and data integrity +tags: jsonb, json, schema-design, performance +--- + +## Use Structured Columns Over JSONB When Possible + +JSONB is flexible but should not replace proper schema design. Use structured +columns for known fields, JSONB for truly dynamic data. + +**Incorrect:** + +```sql +-- Everything in JSONB - loses type safety and performance +create table users ( + id uuid primary key, + data jsonb -- contains email, name, role, etc. +); + +-- Querying is verbose and slow without indexes +select data ->> 'email' from users +where data ->> 'role' = 'admin'; +``` + +**Correct:** + +```sql +-- Structured columns for known fields +create table users ( + id uuid primary key, + email text not null, + name text, + role text check (role in ('admin', 'user', 'guest')), + -- JSONB only for truly flexible data + preferences jsonb default '{}' +); + +-- Fast, type-safe queries +select email from users where role = 'admin'; +``` + +## When JSONB is Appropriate + +- Webhook payloads +- User-defined fields +- API responses to cache +- Rapid prototyping (migrate to columns later) + +## Indexing JSONB + +```sql +-- GIN index for containment queries +create index idx_users_preferences on users using gin(preferences); + +-- Query using containment operator +select * from users +where preferences @> '{"theme": "dark"}'; +``` + +## Validate JSONB with pg_jsonschema + +```sql +create extension if not exists pg_jsonschema with schema extensions; + +alter table users +add constraint check_preferences check ( + jsonb_matches_schema( + '{ + "type": "object", + "properties": { + "theme": {"type": "string", "enum": ["light", "dark"]}, + "notifications": {"type": "boolean"} + } + }', + preferences + ) +); +``` + +## Querying JSONB + +```javascript +// supabase-js +const { data } = await supabase + .from("users") + .select("email, preferences->theme") + .eq("preferences->>notifications", "true"); +``` + +## Related + +- [perf-indexes.md](perf-indexes.md) +- [Docs](https://supabase.com/docs/guides/database/json) diff --git a/skills/supabase/references/db/schema-realtime.md b/skills/supabase/references/db/schema-realtime.md new file mode 100644 index 0000000..db4a9fb --- /dev/null +++ b/skills/supabase/references/db/schema-realtime.md @@ -0,0 +1,91 @@ +--- +title: Realtime Requires Primary Keys +impact: MEDIUM-HIGH +impactDescription: Prevents Realtime subscription failures and data sync issues +tags: realtime, primary-keys, subscriptions +--- + +## Realtime Requires Primary Keys + +Supabase Realtime uses primary keys to track row changes. Tables without primary +keys cannot be subscribed to. + +**Incorrect:** + +```sql +-- No primary key - Realtime subscriptions will fail +create table messages ( + user_id uuid, + content text, + created_at timestamptz default now() +); +``` + +**Correct:** + +```sql +create table messages ( + id bigint primary key generated always as identity, + user_id uuid references auth.users(id) on delete cascade, + content text not null, + created_at timestamptz default now() +); +``` + +## Enable Realtime for a Table + +**Via SQL:** + +```sql +-- Add table to realtime publication +alter publication supabase_realtime add table messages; +``` + +**Via Dashboard:** + +Database > Publications > supabase_realtime > Add table + +## Realtime with RLS + +RLS policies apply to Realtime subscriptions. Users only receive changes they +have access to. + +```sql +-- Policy applies to realtime +create policy "Users see own messages" on messages + for select to authenticated + using (user_id = (select auth.uid())); +``` + +```javascript +// Subscribe with RLS filtering +const channel = supabase + .channel("messages") + .on( + "postgres_changes", + { event: "*", schema: "public", table: "messages" }, + (payload) => console.log(payload) + ) + .subscribe(); +``` + +## Performance Considerations + +- Add indexes on columns used in Realtime filters +- Keep RLS policies simple for subscribed tables +- Monitor "Realtime Private Channel RLS Execution Time" in Dashboard + +## Replica Identity + +By default, only the primary key is sent in UPDATE/DELETE payloads. To receive +all columns: + +```sql +-- Send all columns in change events (increases bandwidth) +alter table messages replica identity full; +``` + +## Related + +- [rls-mandatory.md](rls-mandatory.md) +- [Docs](https://supabase.com/docs/guides/realtime) diff --git a/skills/supabase/references/db/schema-timestamps.md b/skills/supabase/references/db/schema-timestamps.md new file mode 100644 index 0000000..2e65834 --- /dev/null +++ b/skills/supabase/references/db/schema-timestamps.md @@ -0,0 +1,79 @@ +--- +title: Always Use timestamptz Not timestamp +impact: MEDIUM-HIGH +impactDescription: Prevents timezone-related bugs and data inconsistencies +tags: timestamps, timestamptz, timezone, data-types +--- + +## Always Use timestamptz Not timestamp + +Use `timestamptz` (timestamp with time zone) instead of `timestamp`. The latter +loses timezone information, causing bugs when users are in different timezones. + +**Incorrect:** + +```sql +create table events ( + id bigint primary key generated always as identity, + name text not null, + -- Stores time without timezone context + created_at timestamp default now(), + starts_at timestamp +); +``` + +**Correct:** + +```sql +create table events ( + id bigint primary key generated always as identity, + name text not null, + -- Stores time in UTC, converts on retrieval + created_at timestamptz default now(), + starts_at timestamptz +); +``` + +## How timestamptz Works + +- Stores time in UTC internally +- Converts to/from session timezone automatically +- `now()` returns current time in session timezone, stored as UTC + +```sql +-- Insert with timezone +insert into events (name, starts_at) +values ('Launch', '2024-03-15 10:00:00-05'); -- EST + +-- Retrieved in UTC by default in Supabase +select starts_at from events; +-- 2024-03-15 15:00:00+00 +``` + +## Auto-Update updated_at Column + +```sql +create table posts ( + id bigint primary key generated always as identity, + title text not null, + created_at timestamptz default now(), + updated_at timestamptz default now() +); + +-- Trigger to auto-update +create or replace function update_updated_at() +returns trigger as $$ +begin + new.updated_at = now(); + return new; +end; +$$ language plpgsql; + +create trigger posts_updated_at + before update on posts + for each row execute function update_updated_at(); +``` + +## Related + +- [Docs](https://supabase.com/docs/guides/database/tables) diff --git a/skills/supabase/references/db/security-functions.md b/skills/supabase/references/db/security-functions.md new file mode 100644 index 0000000..cd1f4fe --- /dev/null +++ b/skills/supabase/references/db/security-functions.md @@ -0,0 +1,125 @@ +--- +title: Use security_definer Functions in Private Schema +impact: HIGH +impactDescription: Controlled privilege escalation without exposing service role +tags: functions, security_definer, security, private-schema +--- + +## Use security_definer Functions in Private Schema + +`security definer` functions run with the privileges of the function owner, not +the caller. Place them in a private schema to prevent direct API access. + +**Incorrect:** + +```sql +-- DANGEROUS: Exposed via API, can be called directly +create function public.get_all_users() +returns setof auth.users +language sql +security definer +as $$ + select * from auth.users; -- Bypasses RLS! +$$; +``` + +**Correct:** + +```sql +-- Create private schema (not exposed to API) +create schema if not exists private; + +-- Function in private schema +create function private.get_all_users() +returns setof auth.users +language sql +security definer +set search_path = '' -- Prevent search_path injection +as $$ + select * from auth.users; +$$; + +-- Wrapper in public schema with access control +create function public.get_user_count() +returns bigint +language sql +security invoker -- Runs as caller +as $$ + select count(*) from private.get_all_users() + where (select auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'; +$$; +``` + +## Common Use Cases + +### 1. Admin Operations + +```sql +create function private.admin_delete_user(target_user_id uuid) +returns void +language plpgsql +security definer +set search_path = '' +as $$ +begin + -- Verify caller is admin + if (select auth.jwt() -> 'app_metadata' ->> 'role') != 'admin' then + raise exception 'Unauthorized'; + end if; + + delete from auth.users where id = target_user_id; +end; +$$; +``` + +### 2. Cross-User Data Access + +```sql +-- Function returns team IDs the current user belongs to +create function private.user_teams() +returns setof uuid +language sql +security definer +stable +set search_path = '' +as $$ + select team_id from public.team_members + where user_id = (select auth.uid()); +$$; + +-- RLS policy uses cached function result (no per-row join) +create policy "Team members see team data" on team_data + for select to authenticated + using (team_id in (select private.user_teams())); +``` + +## Security Best Practices + +1. **Always set search_path = ''** - Prevents search_path injection attacks +2. **Validate caller permissions** - Don't assume caller is authorized +3. **Keep functions minimal** - Only expose necessary operations +4. **Log sensitive operations** - Audit trail for admin actions + +```sql +create function private.sensitive_operation() +returns void +language plpgsql +security definer +set search_path = '' +as $$ +begin + -- Log the operation + insert into audit_log (user_id, action, timestamp) + values ((select auth.uid()), 'sensitive_operation', now()); + + -- Perform operation + -- ... +end; +$$; +``` + +## Related + +- [security-service-role.md](security-service-role.md) +- [rls-performance.md](rls-performance.md) +- [Docs](https://supabase.com/docs/guides/database/functions) diff --git a/skills/supabase/references/db/security-service-role.md b/skills/supabase/references/db/security-service-role.md new file mode 100644 index 0000000..5faae9a --- /dev/null +++ b/skills/supabase/references/db/security-service-role.md @@ -0,0 +1,97 @@ +--- +title: Never Expose Service Role Key to Browser +impact: CRITICAL +impactDescription: Prevents complete database compromise and data breach +tags: service-role, security, api-keys, anon-key +--- + +## Never Expose Service Role Key to Browser + +The service role key bypasses ALL Row Level Security. Exposing it gives complete +database access to anyone. + +**Incorrect:** + +```javascript +// NEVER do this - service key in frontend code! +const supabase = createClient( + "https://xxx.supabase.co", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // service_role key +); +``` + +**Correct:** + +```javascript +// Browser: Use anon key (respects RLS) +const supabase = createClient( + "https://xxx.supabase.co", + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // anon key +); +``` + +## When to Use Service Role Key + +Only in server-side code that users cannot access: + +```javascript +// Edge Function or backend server +import { createClient } from "@supabase/supabase-js"; + +const supabaseAdmin = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY // Only in secure backend +); + +// Bypass RLS for admin operations +const { data } = await supabaseAdmin.from("users").select("*"); +``` + +## Environment Variables + +```bash +## .env.local (never commit to git!) +NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # Safe to expose +SUPABASE_SERVICE_ROLE_KEY=eyJ... # NEVER prefix with NEXT_PUBLIC_ +``` + +## Identifying Keys + +- **Anon key**: `role` claim is `anon` +- **Service role key**: `role` claim is `service_role` + +Decode JWT at [jwt.io](https://jwt.io) to verify. + +## If Service Key is Exposed + +1. Immediately rotate keys in Dashboard > Settings > API +2. Review database for unauthorized changes +3. Check logs for suspicious activity +4. Update all backend services with new key + +## Alternative: Security Definer Functions + +Instead of service role, use security definer functions for specific elevated +operations: + +```sql +-- Runs with function owner's privileges +create function admin_get_user_count() +returns bigint +language sql +security definer +set search_path = '' +as $$ + select count(*) from auth.users; +$$; + +-- Grant to authenticated users +grant execute on function admin_get_user_count to authenticated; +``` + +## Related + +- [security-functions.md](security-functions.md) +- [rls-mandatory.md](rls-mandatory.md) +- [Docs](https://supabase.com/docs/guides/api/api-keys) diff --git a/test/sanity.test.ts b/test/sanity.test.ts new file mode 100644 index 0000000..e40d791 --- /dev/null +++ b/test/sanity.test.ts @@ -0,0 +1,106 @@ +import { execSync } from "node:child_process"; +import { existsSync, readdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +const SKILLS_DIR = join(__dirname, "..", "skills"); +const CLAUDE_SKILLS_DIR = join(__dirname, "..", ".claude", "skills"); + +/** + * Dynamically discover all skill names from the skills/ directory + */ +function discoverSkillNames(): string[] { + if (!existsSync(SKILLS_DIR)) { + return []; + } + + return readdirSync(SKILLS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .filter((entry) => existsSync(join(SKILLS_DIR, entry.name, "SKILL.md"))) + .map((entry) => entry.name); +} + +describe("skills add sanity check", () => { + let commandOutput: string; + let commandExitCode: number; + const skillNames = discoverSkillNames(); + + beforeAll(() => { + // Clean up any existing .claude/skills directory + if (existsSync(CLAUDE_SKILLS_DIR)) { + rmSync(CLAUDE_SKILLS_DIR, { recursive: true, force: true }); + } + + // Run the skills add command using current directory (.) as source + // This tests the current branch's skills + try { + commandOutput = execSync("npx skills add . -a claude-code -y", { + cwd: join(__dirname, ".."), + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + timeout: 120000, // 2 minute timeout + }); + commandExitCode = 0; + } catch (error) { + const execError = error as { + stdout?: string; + stderr?: string; + status?: number; + }; + commandOutput = `${execError.stdout || ""}\n${execError.stderr || ""}`; + commandExitCode = execError.status ?? 1; + } + }); + + afterAll(() => { + // Clean up .claude/skills directory after tests + if (existsSync(CLAUDE_SKILLS_DIR)) { + rmSync(CLAUDE_SKILLS_DIR, { recursive: true, force: true }); + } + }); + + it("should have discovered skills in the repository", () => { + expect(skillNames.length).toBeGreaterThan(0); + console.log( + `Discovered ${skillNames.length} skills: ${skillNames.join(", ")}`, + ); + }); + + it("should not contain 'Error' in command output", () => { + // Check for error patterns in output (case-insensitive for common error messages) + const hasError = + /\bError\b/i.test(commandOutput) && !/✓/.test(commandOutput); + + if (hasError) { + console.log("Command output:", commandOutput); + } + + // Allow output with errors if the command still succeeded + // Some tools output "Error" in informational messages + expect(commandExitCode).toBe(0); + }); + + it("should create .claude/skills directory", () => { + expect(existsSync(CLAUDE_SKILLS_DIR)).toBe(true); + }); + + it("should install all skills from the repository", () => { + for (const skillName of skillNames) { + const skillPath = join(CLAUDE_SKILLS_DIR, skillName); + expect( + existsSync(skillPath), + `Expected skill "${skillName}" to be installed at ${skillPath}`, + ).toBe(true); + } + }); + + it("should have SKILL.md in each installed skill", () => { + for (const skillName of skillNames) { + const skillMdPath = join(CLAUDE_SKILLS_DIR, skillName, "SKILL.md"); + expect( + existsSync(skillMdPath), + `Expected SKILL.md to exist at ${skillMdPath}`, + ).toBe(true); + } + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..146dd0c --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 180000, // 3 minute timeout for sanity tests + hookTimeout: 180000, + }, +});