Update AGENTS.md
This commit is contained in:
+235
@@ -0,0 +1,235 @@
|
||||
# API Documentation
|
||||
|
||||
Only read this file when modifying API routes, authentication, keys, or integrations.
|
||||
|
||||
---
|
||||
|
||||
# API Location
|
||||
|
||||
API routes:
|
||||
|
||||
src/routes/api/v1/
|
||||
|
||||
---
|
||||
|
||||
# Authentication
|
||||
|
||||
Most API endpoints require:
|
||||
|
||||
Authorization: Bearer <API key>
|
||||
|
||||
API keys are:
|
||||
|
||||
- generated randomly
|
||||
- hashed with SHA-256 before storage
|
||||
- displayed using stored prefix only
|
||||
|
||||
Never store plaintext API keys.
|
||||
|
||||
---
|
||||
|
||||
# API Key Lifecycle
|
||||
|
||||
Normal rotation:
|
||||
|
||||
- New key generated.
|
||||
- Old key remains valid for 48 hours.
|
||||
- Old key expires using expiresAt.
|
||||
|
||||
Emergency rotation:
|
||||
|
||||
- Old key immediately invalidated.
|
||||
- revokedAt is set.
|
||||
|
||||
---
|
||||
|
||||
# API Routes
|
||||
|
||||
## Categories
|
||||
|
||||
GET:
|
||||
|
||||
/api/v1/categories
|
||||
|
||||
Requires API key.
|
||||
|
||||
---
|
||||
|
||||
## Frequencies
|
||||
|
||||
GET:
|
||||
|
||||
/api/v1/frequencies
|
||||
|
||||
Requires API key.
|
||||
|
||||
---
|
||||
|
||||
## Proclamations
|
||||
|
||||
GET:
|
||||
|
||||
/api/v1/proclamations
|
||||
|
||||
Supports:
|
||||
|
||||
- optional key filtering
|
||||
- optional email filtering
|
||||
|
||||
Requires API key.
|
||||
|
||||
---
|
||||
|
||||
## License Validation
|
||||
|
||||
POST:
|
||||
|
||||
/api/v1/license
|
||||
|
||||
Payload:
|
||||
|
||||
{
|
||||
"license_key": "...",
|
||||
"email_address": "..."
|
||||
}
|
||||
|
||||
Requires API key.
|
||||
|
||||
---
|
||||
|
||||
# GHL Account Creation Webhook
|
||||
|
||||
Endpoint:
|
||||
|
||||
POST /api/v1/automations/create/account
|
||||
|
||||
This endpoint does NOT require an API key.
|
||||
|
||||
It is used by GoHighLevel automation.
|
||||
|
||||
---
|
||||
|
||||
# Account Creation Flow
|
||||
|
||||
When a request arrives:
|
||||
|
||||
1. Check whether the email already exists.
|
||||
|
||||
If user does not exist:
|
||||
|
||||
2. Create user through Better Auth.
|
||||
3. Generate temporary password.
|
||||
4. Retrieve verification token.
|
||||
5. Build verification URL.
|
||||
6. Send registration email.
|
||||
|
||||
Then:
|
||||
|
||||
7. Create LicenseKey.
|
||||
8. Send license key delivery email.
|
||||
|
||||
If user already exists:
|
||||
|
||||
1. Skip signup.
|
||||
2. Create license key.
|
||||
3. Send delivery email.
|
||||
|
||||
---
|
||||
|
||||
# Email Templates
|
||||
|
||||
Controlled by:
|
||||
|
||||
src/lib/server/resend.ts
|
||||
|
||||
Templates:
|
||||
|
||||
automated-user-registration
|
||||
|
||||
Variables:
|
||||
|
||||
- firstName
|
||||
- tempPassword
|
||||
- verifyUrl
|
||||
- loginUrl
|
||||
|
||||
|
||||
license-key-delivery
|
||||
|
||||
Variables:
|
||||
|
||||
- firstName
|
||||
- license_key
|
||||
|
||||
|
||||
email-verification-email
|
||||
|
||||
Variables:
|
||||
|
||||
- target_email_address
|
||||
- verification_link
|
||||
|
||||
---
|
||||
|
||||
# Better Auth
|
||||
|
||||
Server:
|
||||
|
||||
src/lib/auth.ts
|
||||
|
||||
Client:
|
||||
|
||||
src/lib/auth-client.ts
|
||||
|
||||
Email callbacks:
|
||||
|
||||
- sendVerificationEmail
|
||||
- sendResetPassword
|
||||
|
||||
Failures should:
|
||||
|
||||
- log errors
|
||||
- not crash requests
|
||||
|
||||
Use:
|
||||
|
||||
void promise.catch()
|
||||
|
||||
pattern.
|
||||
|
||||
---
|
||||
|
||||
# Security Rules
|
||||
|
||||
Never:
|
||||
|
||||
- expose plaintext API keys
|
||||
- bypass authorization checks
|
||||
- trust client role information
|
||||
- skip validation
|
||||
|
||||
Always:
|
||||
|
||||
- validate input
|
||||
- check permissions
|
||||
- preserve existing security patterns
|
||||
|
||||
---
|
||||
|
||||
# API Change Workflow
|
||||
|
||||
Before modifying an endpoint:
|
||||
|
||||
1. Read the route.
|
||||
2. Read related auth middleware.
|
||||
3. Read database models involved.
|
||||
4. Check existing tests.
|
||||
|
||||
After modifying:
|
||||
|
||||
Run:
|
||||
|
||||
npm run check
|
||||
npm run test
|
||||
|
||||
Fix failures before completing.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Svelte 5 Patterns and Rules
|
||||
|
||||
This document contains Svelte-specific implementation rules.
|
||||
|
||||
Only read this file when modifying Svelte components, routes, or Svelte-specific logic.
|
||||
|
||||
---
|
||||
|
||||
# Svelte Version
|
||||
|
||||
This project uses:
|
||||
|
||||
- SvelteKit 2
|
||||
- Svelte 5
|
||||
- runes mode
|
||||
|
||||
Always use:
|
||||
|
||||
- $state()
|
||||
- $derived()
|
||||
- $effect()
|
||||
- $props()
|
||||
|
||||
Do not use legacy stores:
|
||||
|
||||
- writable()
|
||||
- readable()
|
||||
- derived()
|
||||
|
||||
---
|
||||
|
||||
# Component Props
|
||||
|
||||
Always destructure $props() with defaults.
|
||||
|
||||
Preferred pattern:
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
isSuper = false
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
isSuper?: boolean;
|
||||
} = $props();
|
||||
|
||||
Do not assume props exist.
|
||||
|
||||
---
|
||||
|
||||
# State Synchronization
|
||||
|
||||
IMPORTANT:
|
||||
|
||||
Do not initialize $state from props when the prop can change.
|
||||
|
||||
Incorrect:
|
||||
|
||||
let name = $state(record.name);
|
||||
|
||||
This only captures the initial value.
|
||||
|
||||
Correct:
|
||||
|
||||
let name = $state('');
|
||||
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
});
|
||||
|
||||
Use $effect whenever component state needs to follow changing props.
|
||||
|
||||
---
|
||||
|
||||
# Effect Ordering
|
||||
|
||||
$effect blocks execute in declaration order.
|
||||
|
||||
When multiple effects modify related state:
|
||||
|
||||
1. Reset state first.
|
||||
2. Populate state second.
|
||||
3. Perform dependent calculations afterward.
|
||||
|
||||
Example:
|
||||
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
name = String(record.name ?? '');
|
||||
});
|
||||
|
||||
This ensures a new record starts clean.
|
||||
|
||||
---
|
||||
|
||||
# Select Inputs
|
||||
|
||||
IMPORTANT:
|
||||
|
||||
Do not use bind:value on select elements whose values are synchronized through $effect.
|
||||
|
||||
Incorrect:
|
||||
|
||||
<select bind:value={frequency}>
|
||||
<option value="1">One</option>
|
||||
</select>
|
||||
|
||||
This can fail to update correctly.
|
||||
|
||||
Use:
|
||||
|
||||
<select onchange={(e) => frequency = e.currentTarget.value}>
|
||||
<option value="1" selected={frequency === '1'}>
|
||||
One
|
||||
</option>
|
||||
</select>
|
||||
|
||||
Use selected on options.
|
||||
|
||||
---
|
||||
|
||||
# Input Elements
|
||||
|
||||
Text inputs are different.
|
||||
|
||||
This is acceptable:
|
||||
|
||||
<input bind:value={name} />
|
||||
|
||||
bind:value works correctly with normal input elements.
|
||||
|
||||
---
|
||||
|
||||
# Forms
|
||||
|
||||
Admin forms use:
|
||||
|
||||
- $state for field values
|
||||
- $effect for prop synchronization
|
||||
- oninput for dirty tracking
|
||||
- use:enhance for submission
|
||||
|
||||
Example:
|
||||
|
||||
<form oninput={markDirty}>
|
||||
<button disabled={!!record?.id && !dirty}>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
|
||||
---
|
||||
|
||||
# Avoid Unnecessary State
|
||||
|
||||
Prefer:
|
||||
|
||||
$derived()
|
||||
|
||||
for values computed from other state.
|
||||
|
||||
Use:
|
||||
|
||||
$state()
|
||||
|
||||
only for values that change independently.
|
||||
|
||||
---
|
||||
|
||||
# Component Changes
|
||||
|
||||
Before modifying a component:
|
||||
|
||||
1. Read the entire component.
|
||||
2. Find similar components.
|
||||
3. Follow existing patterns.
|
||||
4. Avoid introducing new abstractions unless necessary.
|
||||
|
||||
Small, consistent changes are preferred.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Testing Patterns
|
||||
|
||||
Only read this file when creating or modifying tests.
|
||||
|
||||
This project uses:
|
||||
|
||||
- Vitest
|
||||
- Playwright browser testing
|
||||
- Svelte Testing Library patterns through vitest/browser
|
||||
|
||||
---
|
||||
|
||||
# Test Philosophy
|
||||
|
||||
Tests should prove behavior.
|
||||
|
||||
Do not write tests that only confirm implementation details.
|
||||
|
||||
Every test must contain meaningful assertions.
|
||||
|
||||
A test without assertions fails because:
|
||||
|
||||
expect.requireAssertions is enabled.
|
||||
|
||||
---
|
||||
|
||||
# Test Locations
|
||||
|
||||
Tests are colocated with source files.
|
||||
|
||||
Client tests:
|
||||
|
||||
*.svelte.test.ts
|
||||
*.svelte.spec.ts
|
||||
|
||||
Server tests:
|
||||
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
|
||||
---
|
||||
|
||||
# Vitest Projects
|
||||
|
||||
There are two test environments.
|
||||
|
||||
## Client
|
||||
|
||||
Used for Svelte components.
|
||||
|
||||
Environment:
|
||||
|
||||
Playwright Chromium browser.
|
||||
|
||||
Used for:
|
||||
|
||||
- rendering
|
||||
- user interaction
|
||||
- form behavior
|
||||
|
||||
---
|
||||
|
||||
## Server
|
||||
|
||||
Used for:
|
||||
|
||||
- load functions
|
||||
- actions
|
||||
- server utilities
|
||||
|
||||
Environment:
|
||||
|
||||
Node.
|
||||
|
||||
---
|
||||
|
||||
# Before Each Test
|
||||
|
||||
Always reset mocks:
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
---
|
||||
|
||||
# Server Tests
|
||||
|
||||
Server route tests should cover:
|
||||
|
||||
- authenticated behavior
|
||||
- unauthenticated behavior
|
||||
- authorization rules
|
||||
- successful operations
|
||||
- failed operations
|
||||
|
||||
Mock:
|
||||
|
||||
- $lib/db
|
||||
- authentication helpers
|
||||
- external services
|
||||
|
||||
---
|
||||
|
||||
# Prisma Mocking
|
||||
|
||||
Mock only the methods used.
|
||||
|
||||
Example:
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
proclamation: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Do not create massive unused mocks.
|
||||
|
||||
---
|
||||
|
||||
# Authentication Testing
|
||||
|
||||
Use:
|
||||
|
||||
createLocals()
|
||||
|
||||
Examples:
|
||||
|
||||
Unauthenticated:
|
||||
|
||||
createLocals()
|
||||
|
||||
Authenticated:
|
||||
|
||||
createLocals(mockUser() as any)
|
||||
|
||||
Specific roles:
|
||||
|
||||
createLocals(
|
||||
mockUser({ roleId: 4 }) as any
|
||||
)
|
||||
|
||||
---
|
||||
|
||||
# Client Test Locators
|
||||
|
||||
Available:
|
||||
|
||||
getByRole
|
||||
getByText
|
||||
getByPlaceholder
|
||||
getByAltText
|
||||
getByTestId
|
||||
|
||||
Unavailable:
|
||||
|
||||
getByLabel
|
||||
getByDisplayValue
|
||||
|
||||
---
|
||||
|
||||
# Role Queries
|
||||
|
||||
Always use exact regex names.
|
||||
|
||||
Preferred:
|
||||
|
||||
page.getByRole(
|
||||
'textbox',
|
||||
{ name: /^Name$/ }
|
||||
)
|
||||
|
||||
Avoid:
|
||||
|
||||
page.getByRole(
|
||||
'textbox',
|
||||
{ name: 'Name' }
|
||||
)
|
||||
|
||||
because substring matching can create strict-mode failures.
|
||||
|
||||
---
|
||||
|
||||
# Select Testing
|
||||
|
||||
Because selects use selected options instead of bind:value:
|
||||
|
||||
const element =
|
||||
await page
|
||||
.getByRole(
|
||||
'combobox',
|
||||
{ name: /^Frequency$/ }
|
||||
)
|
||||
.element();
|
||||
|
||||
expect(
|
||||
(element as HTMLSelectElement).value
|
||||
).toBe('2');
|
||||
|
||||
---
|
||||
|
||||
# Svelte Component Mocking
|
||||
|
||||
When mocking Svelte 5 components:
|
||||
|
||||
Use:
|
||||
|
||||
vi.mock('$lib/components/example.svelte', () => ({
|
||||
default: ($anchor: Element) => {
|
||||
const div = document.createElement('div');
|
||||
$anchor.replaceWith(div);
|
||||
}
|
||||
}));
|
||||
|
||||
Imports using mocked modules must come after vi.mock().
|
||||
|
||||
If variables are needed inside mocks:
|
||||
|
||||
Use vi.hoisted().
|
||||
|
||||
---
|
||||
|
||||
# Test Completion Rule
|
||||
|
||||
After writing tests:
|
||||
|
||||
Run:
|
||||
|
||||
npm run test
|
||||
|
||||
If failures occur:
|
||||
|
||||
Fix them.
|
||||
|
||||
Do not report "tests added" without verifying they pass.
|
||||
Reference in New Issue
Block a user