@levironexe/architect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +55 -0
- package/README.md +341 -0
- package/dist/analyzers/ast-parser.d.ts +3 -0
- package/dist/analyzers/ast-parser.js +305 -0
- package/dist/analyzers/ast-parser.js.map +1 -0
- package/dist/analyzers/dependency-graph.d.ts +2 -0
- package/dist/analyzers/dependency-graph.js +67 -0
- package/dist/analyzers/dependency-graph.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +56 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/file-walker.d.ts +3 -0
- package/dist/analyzers/file-walker.js +80 -0
- package/dist/analyzers/file-walker.js.map +1 -0
- package/dist/cli/context-runner.d.ts +1 -0
- package/dist/cli/context-runner.js +16 -0
- package/dist/cli/context-runner.js.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.js +217 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-runner.d.ts +25 -0
- package/dist/cli/init-runner.js +152 -0
- package/dist/cli/init-runner.js.map +1 -0
- package/dist/cli/scan-runner.d.ts +8 -0
- package/dist/cli/scan-runner.js +133 -0
- package/dist/cli/scan-runner.js.map +1 -0
- package/dist/formatters/plan-json.d.ts +2 -0
- package/dist/formatters/plan-json.js +4 -0
- package/dist/formatters/plan-json.js.map +1 -0
- package/dist/formatters/plan-markdown.d.ts +2 -0
- package/dist/formatters/plan-markdown.js +42 -0
- package/dist/formatters/plan-markdown.js.map +1 -0
- package/dist/formatters/plan-prompt.d.ts +4 -0
- package/dist/formatters/plan-prompt.js +5 -0
- package/dist/formatters/plan-prompt.js.map +1 -0
- package/dist/formatters/plan-terminal.d.ts +5 -0
- package/dist/formatters/plan-terminal.js +62 -0
- package/dist/formatters/plan-terminal.js.map +1 -0
- package/dist/generators/blueprint-renderer.d.ts +3 -0
- package/dist/generators/blueprint-renderer.js +27 -0
- package/dist/generators/blueprint-renderer.js.map +1 -0
- package/dist/generators/claudeWriter.d.ts +3 -0
- package/dist/generators/claudeWriter.js +9 -0
- package/dist/generators/claudeWriter.js.map +1 -0
- package/dist/generators/copilotWriter.d.ts +3 -0
- package/dist/generators/copilotWriter.js +11 -0
- package/dist/generators/copilotWriter.js.map +1 -0
- package/dist/generators/cursorWriter.d.ts +3 -0
- package/dist/generators/cursorWriter.js +14 -0
- package/dist/generators/cursorWriter.js.map +1 -0
- package/dist/generators/genericWriter.d.ts +3 -0
- package/dist/generators/genericWriter.js +9 -0
- package/dist/generators/genericWriter.js.map +1 -0
- package/dist/generators/template-context.d.ts +18 -0
- package/dist/generators/template-context.js +126 -0
- package/dist/generators/template-context.js.map +1 -0
- package/dist/generators/templateRenderer.d.ts +2 -0
- package/dist/generators/templateRenderer.js +19 -0
- package/dist/generators/templateRenderer.js.map +1 -0
- package/dist/generators/windsurfWriter.d.ts +3 -0
- package/dist/generators/windsurfWriter.js +14 -0
- package/dist/generators/windsurfWriter.js.map +1 -0
- package/dist/generators/writer-types.d.ts +11 -0
- package/dist/generators/writer-types.js +40 -0
- package/dist/generators/writer-types.js.map +1 -0
- package/dist/llm/claude-provider.d.ts +8 -0
- package/dist/llm/claude-provider.js +22 -0
- package/dist/llm/claude-provider.js.map +1 -0
- package/dist/llm/concern-classifier.d.ts +15 -0
- package/dist/llm/concern-classifier.js +61 -0
- package/dist/llm/concern-classifier.js.map +1 -0
- package/dist/llm/config.d.ts +11 -0
- package/dist/llm/config.js +120 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/ollama-provider.d.ts +8 -0
- package/dist/llm/ollama-provider.js +27 -0
- package/dist/llm/ollama-provider.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +8 -0
- package/dist/llm/openai-provider.js +19 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/prompt-builder.d.ts +12 -0
- package/dist/llm/prompt-builder.js +132 -0
- package/dist/llm/prompt-builder.js.map +1 -0
- package/dist/llm/provider.d.ts +17 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/llm/response-parser.d.ts +6 -0
- package/dist/llm/response-parser.js +128 -0
- package/dist/llm/response-parser.js.map +1 -0
- package/dist/planner/plan-generator.d.ts +7 -0
- package/dist/planner/plan-generator.js +275 -0
- package/dist/planner/plan-generator.js.map +1 -0
- package/dist/planner/plan-prompt-builder.d.ts +9 -0
- package/dist/planner/plan-prompt-builder.js +92 -0
- package/dist/planner/plan-prompt-builder.js.map +1 -0
- package/dist/planner/plan-response-parser.d.ts +7 -0
- package/dist/planner/plan-response-parser.js +21 -0
- package/dist/planner/plan-response-parser.js.map +1 -0
- package/dist/planner/plan-validator.d.ts +3 -0
- package/dist/planner/plan-validator.js +49 -0
- package/dist/planner/plan-validator.js.map +1 -0
- package/dist/reporters/scan-json.d.ts +13 -0
- package/dist/reporters/scan-json.js +26 -0
- package/dist/reporters/scan-json.js.map +1 -0
- package/dist/reporters/terminal.d.ts +6 -0
- package/dist/reporters/terminal.js +224 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/scoring/consistency-score.d.ts +3 -0
- package/dist/scoring/consistency-score.js +23 -0
- package/dist/scoring/consistency-score.js.map +1 -0
- package/dist/scoring/duplication-score.d.ts +3 -0
- package/dist/scoring/duplication-score.js +16 -0
- package/dist/scoring/duplication-score.js.map +1 -0
- package/dist/scoring/health-score.d.ts +4 -0
- package/dist/scoring/health-score.js +20 -0
- package/dist/scoring/health-score.js.map +1 -0
- package/dist/scoring/issue-builder.d.ts +4 -0
- package/dist/scoring/issue-builder.js +62 -0
- package/dist/scoring/issue-builder.js.map +1 -0
- package/dist/scoring/modularity-score.d.ts +3 -0
- package/dist/scoring/modularity-score.js +56 -0
- package/dist/scoring/modularity-score.js.map +1 -0
- package/dist/scoring/pattern-analysis.d.ts +3 -0
- package/dist/scoring/pattern-analysis.js +74 -0
- package/dist/scoring/pattern-analysis.js.map +1 -0
- package/dist/scoring/separation-score.d.ts +3 -0
- package/dist/scoring/separation-score.js +35 -0
- package/dist/scoring/separation-score.js.map +1 -0
- package/dist/skills/detector.d.ts +4 -0
- package/dist/skills/detector.js +104 -0
- package/dist/skills/detector.js.map +1 -0
- package/dist/skills/lister.d.ts +9 -0
- package/dist/skills/lister.js +35 -0
- package/dist/skills/lister.js.map +1 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +76 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/structure-check.d.ts +2 -0
- package/dist/skills/structure-check.js +37 -0
- package/dist/skills/structure-check.js.map +1 -0
- package/dist/skills/validator.d.ts +6 -0
- package/dist/skills/validator.js +229 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/types/analysis.d.ts +130 -0
- package/dist/types/analysis.js +41 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/concern.d.ts +48 -0
- package/dist/types/concern.js +16 -0
- package/dist/types/concern.js.map +1 -0
- package/dist/types/generation.d.ts +32 -0
- package/dist/types/generation.js +2 -0
- package/dist/types/generation.js.map +1 -0
- package/dist/types/issue.d.ts +12 -0
- package/dist/types/issue.js +2 -0
- package/dist/types/issue.js.map +1 -0
- package/dist/types/pattern.d.ts +15 -0
- package/dist/types/pattern.js +2 -0
- package/dist/types/pattern.js.map +1 -0
- package/dist/types/plan.d.ts +56 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/scan-output.d.ts +84 -0
- package/dist/types/scan-output.js +2 -0
- package/dist/types/scan-output.js.map +1 -0
- package/dist/types/scoring.d.ts +15 -0
- package/dist/types/scoring.js +2 -0
- package/dist/types/scoring.js.map +1 -0
- package/dist/types/skill.d.ts +97 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/utils/agent-detector.d.ts +2 -0
- package/dist/utils/agent-detector.js +22 -0
- package/dist/utils/agent-detector.js.map +1 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +15 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +31 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/progress.d.ts +17 -0
- package/dist/utils/progress.js +48 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/thresholds.d.ts +6 -0
- package/dist/utils/thresholds.js +48 -0
- package/dist/utils/thresholds.js.map +1 -0
- package/package.json +63 -0
- package/skills/meta/general-js.skill.yaml +131 -0
- package/skills/patterns/clerk-auth.skill.yaml +349 -0
- package/skills/patterns/docker-deploy.skill.yaml +214 -0
- package/skills/patterns/drizzle.skill.yaml +277 -0
- package/skills/patterns/mongoose.skill.yaml +290 -0
- package/skills/patterns/nextauth.skill.yaml +308 -0
- package/skills/patterns/playwright-e2e.skill.yaml +265 -0
- package/skills/patterns/prisma.skill.yaml +255 -0
- package/skills/patterns/s3-storage.skill.yaml +235 -0
- package/skills/patterns/selenium-e2e.skill.yaml +276 -0
- package/skills/patterns/supabase-auth.skill.yaml +298 -0
- package/skills/patterns/supabase.skill.yaml +304 -0
- package/skills/patterns/vercel-deploy.skill.yaml +219 -0
- package/skills/patterns/vitest-testing.skill.yaml +262 -0
- package/skills/stacks/express-api.skill.yaml +155 -0
- package/skills/stacks/fastify-api.skill.yaml +119 -0
- package/skills/stacks/hono-api.skill.yaml +130 -0
- package/skills/stacks/nestjs.skill.yaml +135 -0
- package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
- package/skills/stacks/react-spa.skill.yaml +153 -0
- package/skills/stacks/vue-nuxt.skill.yaml +115 -0
- package/templates/architect-plan.md +139 -0
- package/templates/architect-refactor.md +119 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: selenium-e2e
|
|
3
|
+
name: "Selenium E2E"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "End-to-end testing with Selenium WebDriver — Page Object Models with BasePage, explicit waits (no sleeps), driver factory, screenshot capture on failure, data-testid selectors, and driver.quit() in afterEach."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- selenium-webdriver
|
|
10
|
+
dependencies:
|
|
11
|
+
none: []
|
|
12
|
+
detection:
|
|
13
|
+
dependencies:
|
|
14
|
+
any:
|
|
15
|
+
- selenium-webdriver
|
|
16
|
+
- "@selenium-devtools/expect-webdriver"
|
|
17
|
+
source_indicators:
|
|
18
|
+
- "from 'selenium-webdriver'"
|
|
19
|
+
- "new Builder()"
|
|
20
|
+
- "driver.findElement("
|
|
21
|
+
- "By.css("
|
|
22
|
+
- "until.elementLocated"
|
|
23
|
+
structure:
|
|
24
|
+
required_dirs:
|
|
25
|
+
- path: tests/e2e
|
|
26
|
+
purpose: "E2E test specs organized by user flow or feature. Each spec file tests one major user journey. Specs import Page Objects from tests/e2e/pages/ and the driver factory from tests/e2e/fixtures/ — they never contain raw driver.findElement() calls or By selectors."
|
|
27
|
+
recommended_dirs:
|
|
28
|
+
- path: tests/e2e/pages
|
|
29
|
+
purpose: "Page Object Model classes — one class per page, extending BasePage. BasePage provides shared explicit wait helpers so individual POMs don't copy-paste wait logic. Example: LoginPage extends BasePage, uses this.waitAndFind('login-form') from BasePage."
|
|
30
|
+
- path: tests/e2e/fixtures
|
|
31
|
+
purpose: "Driver factory (createDriver.ts) and shared setup helpers. createDriver() is the only place new Builder() is called — tests import and call createDriver() rather than building the WebDriver themselves."
|
|
32
|
+
separation:
|
|
33
|
+
rules:
|
|
34
|
+
- concern: page_object_models
|
|
35
|
+
belongs_in: tests/e2e/pages
|
|
36
|
+
rule_text: "All locators and user-action sequences go in Page Object Model classes. POM classes extend BasePage which provides shared explicit wait helpers. Tests call high-level POM methods (login(), submitContactForm()) — they never call driver.findElement() or By directly."
|
|
37
|
+
example: |
|
|
38
|
+
// tests/e2e/pages/base.page.ts
|
|
39
|
+
import { WebDriver, By, until, WebElement } from 'selenium-webdriver';
|
|
40
|
+
|
|
41
|
+
export class BasePage {
|
|
42
|
+
constructor(protected driver: WebDriver) {}
|
|
43
|
+
|
|
44
|
+
protected async waitAndFind(testId: string, timeout = 5000): Promise<WebElement> {
|
|
45
|
+
const locator = By.css(`[data-testid="${testId}"]`);
|
|
46
|
+
await this.driver.wait(until.elementLocated(locator), timeout);
|
|
47
|
+
return this.driver.findElement(locator);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected async waitForUrl(urlFragment: string, timeout = 5000) {
|
|
51
|
+
await this.driver.wait(
|
|
52
|
+
async () => (await this.driver.getCurrentUrl()).includes(urlFragment),
|
|
53
|
+
timeout,
|
|
54
|
+
`URL did not contain "${urlFragment}" within ${timeout}ms`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// tests/e2e/pages/login.page.ts
|
|
60
|
+
import { By } from 'selenium-webdriver';
|
|
61
|
+
import { BasePage } from './base.page';
|
|
62
|
+
|
|
63
|
+
export class LoginPage extends BasePage {
|
|
64
|
+
async goto() {
|
|
65
|
+
await this.driver.get(`${process.env.BASE_URL ?? 'http://localhost:3000'}/login`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async login(email: string, password: string) {
|
|
69
|
+
const emailInput = await this.waitAndFind('email-input');
|
|
70
|
+
const passwordInput = await this.waitAndFind('password-input');
|
|
71
|
+
await emailInput.sendKeys(email);
|
|
72
|
+
await passwordInput.sendKeys(password);
|
|
73
|
+
const submitBtn = await this.waitAndFind('login-submit');
|
|
74
|
+
await submitBtn.click();
|
|
75
|
+
await this.waitForUrl('/dashboard');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getErrorMessage() {
|
|
79
|
+
const alert = await this.waitAndFind('error-alert');
|
|
80
|
+
return alert.getText();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
indicators:
|
|
84
|
+
- "class LoginPage"
|
|
85
|
+
- "extends BasePage"
|
|
86
|
+
- "new LoginPage(driver)"
|
|
87
|
+
- concern: explicit_waits
|
|
88
|
+
belongs_in: tests/e2e/pages
|
|
89
|
+
rule_text: "Always use explicit waits (driver.wait with until.*) for every element interaction. Never use driver.sleep() or setTimeout() — they add fixed delay even when the element is ready, making tests slow and still flaky. Set a consistent timeout (5-10s for most elements, longer for page transitions)."
|
|
90
|
+
example: |
|
|
91
|
+
import { until, By, WebDriver } from 'selenium-webdriver';
|
|
92
|
+
|
|
93
|
+
// ✓ Explicit wait — resolves immediately when element appears, fails after timeout
|
|
94
|
+
const locator = By.css('[data-testid="result-list"]');
|
|
95
|
+
await driver.wait(until.elementLocated(locator), 8000, 'Result list not found within 8s');
|
|
96
|
+
const el = await driver.findElement(locator);
|
|
97
|
+
await driver.wait(until.elementIsVisible(el), 3000);
|
|
98
|
+
|
|
99
|
+
// ✓ Wait for element to become clickable
|
|
100
|
+
await driver.wait(until.elementIsEnabled(el), 3000);
|
|
101
|
+
await el.click();
|
|
102
|
+
|
|
103
|
+
// ❌ Fixed sleep — arbitrary, slow, still flaky:
|
|
104
|
+
// await driver.sleep(3000);
|
|
105
|
+
// await driver.findElement(locator); // may still fail if 3s wasn't enough
|
|
106
|
+
indicators:
|
|
107
|
+
- "driver.wait("
|
|
108
|
+
- "until.elementLocated"
|
|
109
|
+
- "until.elementIsVisible"
|
|
110
|
+
- concern: driver_factory
|
|
111
|
+
belongs_in: tests/e2e/fixtures
|
|
112
|
+
rule_text: "Create and configure the WebDriver instance in a single factory function exported from tests/e2e/fixtures/create-driver.ts. Test files import createDriver() — they never call `new Builder()`. This centralizes browser configuration (headless mode in CI, device emulation, etc.) in one place."
|
|
113
|
+
example: |
|
|
114
|
+
// tests/e2e/fixtures/create-driver.ts
|
|
115
|
+
import { Builder, WebDriver, Capabilities } from 'selenium-webdriver';
|
|
116
|
+
import chrome from 'selenium-webdriver/chrome';
|
|
117
|
+
|
|
118
|
+
export async function createDriver(): Promise<WebDriver> {
|
|
119
|
+
const options = new chrome.Options();
|
|
120
|
+
if (process.env.CI) {
|
|
121
|
+
options.addArguments('--headless', '--no-sandbox', '--disable-dev-shm-usage');
|
|
122
|
+
}
|
|
123
|
+
return new Builder()
|
|
124
|
+
.forBrowser('chrome')
|
|
125
|
+
.setChromeOptions(options)
|
|
126
|
+
.build();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// tests/e2e/auth.spec.ts
|
|
130
|
+
import { createDriver } from '../fixtures/create-driver';
|
|
131
|
+
let driver: WebDriver;
|
|
132
|
+
beforeEach(async () => { driver = await createDriver(); });
|
|
133
|
+
afterEach(async () => {
|
|
134
|
+
try { await driver.quit(); } catch { /* already closed */ }
|
|
135
|
+
});
|
|
136
|
+
indicators:
|
|
137
|
+
- "createDriver"
|
|
138
|
+
- "new Builder()"
|
|
139
|
+
- "forBrowser("
|
|
140
|
+
- concern: screenshot_on_failure
|
|
141
|
+
belongs_in: tests/e2e
|
|
142
|
+
rule_text: "Capture a screenshot and save it to disk in the afterEach (or afterAll) error handler. In CI, screenshots are the primary debugging tool — without them, a failing test in a headless browser is nearly impossible to diagnose."
|
|
143
|
+
example: |
|
|
144
|
+
// tests/e2e/helpers/screenshot.ts
|
|
145
|
+
import { WebDriver } from 'selenium-webdriver';
|
|
146
|
+
import fs from 'fs';
|
|
147
|
+
import path from 'path';
|
|
148
|
+
|
|
149
|
+
export async function screenshotOnFailure(driver: WebDriver, testName: string) {
|
|
150
|
+
const screenshot = await driver.takeScreenshot();
|
|
151
|
+
const dir = path.resolve('test-screenshots');
|
|
152
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
153
|
+
const filename = `${testName.replace(/\s+/g, '-')}-${Date.now()}.png`;
|
|
154
|
+
fs.writeFileSync(path.join(dir, filename), screenshot, 'base64');
|
|
155
|
+
console.log(`Screenshot saved: ${path.join(dir, filename)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// In test file:
|
|
159
|
+
afterEach(async function() {
|
|
160
|
+
// Mocha: this.currentTest.state === 'failed'
|
|
161
|
+
if (this.currentTest?.state === 'failed') {
|
|
162
|
+
await screenshotOnFailure(driver, this.currentTest.fullTitle());
|
|
163
|
+
}
|
|
164
|
+
await driver.quit();
|
|
165
|
+
});
|
|
166
|
+
indicators:
|
|
167
|
+
- "takeScreenshot"
|
|
168
|
+
- "writeFileSync"
|
|
169
|
+
- "test-screenshots"
|
|
170
|
+
patterns:
|
|
171
|
+
data_flow:
|
|
172
|
+
direction: "Test → POM Methods (explicit waits) → WebDriver → Browser → Application"
|
|
173
|
+
rules:
|
|
174
|
+
- "createDriver() in tests/e2e/fixtures/ is the only place new Builder() is called."
|
|
175
|
+
- "BasePage provides shared waitAndFind() and waitForUrl() — no copy-pasted wait logic in POMs."
|
|
176
|
+
- "Tests call POM methods — never raw driver.findElement() or By selectors."
|
|
177
|
+
- "driver.quit() runs in afterEach/finally — no orphaned browser processes."
|
|
178
|
+
- "Screenshots are saved on test failure for CI debugging."
|
|
179
|
+
error_handling:
|
|
180
|
+
recommended: "Wrap driver.quit() in try/catch in afterEach — if quit fails (already closed), the test runner still continues. Never let a failed driver.quit() block reporting of the actual test failure."
|
|
181
|
+
naming:
|
|
182
|
+
specs: "tests/e2e/[feature].spec.ts"
|
|
183
|
+
base_page: "tests/e2e/pages/base.page.ts — BasePage class with shared wait helpers"
|
|
184
|
+
page_objects: "tests/e2e/pages/[page].page.ts — extends BasePage"
|
|
185
|
+
factory: "tests/e2e/fixtures/create-driver.ts — createDriver() function"
|
|
186
|
+
screenshots: "test-screenshots/[test-name]-[timestamp].png"
|
|
187
|
+
anti_patterns:
|
|
188
|
+
- id: raw_selectors_in_tests
|
|
189
|
+
severity: warning
|
|
190
|
+
description: "Using driver.findElement() and By selectors directly in test spec files instead of Page Object Model methods. When a selector changes, every test that uses it breaks — with POMs, only one file needs updating."
|
|
191
|
+
bad_example: |
|
|
192
|
+
// ❌ Raw locators in test file — breaks when selector changes
|
|
193
|
+
it('can log in', async () => {
|
|
194
|
+
await driver.findElement(By.css('#email-field')).sendKeys('user@test.com');
|
|
195
|
+
await driver.findElement(By.css('#password-field')).sendKeys('password');
|
|
196
|
+
await driver.findElement(By.css('[type="submit"]')).click();
|
|
197
|
+
});
|
|
198
|
+
good_example: |
|
|
199
|
+
// ✓ POM method — one place to update when UI changes
|
|
200
|
+
it('can log in', async () => {
|
|
201
|
+
await loginPage.login('user@test.com', 'password');
|
|
202
|
+
});
|
|
203
|
+
- id: implicit_or_fixed_waits
|
|
204
|
+
severity: critical
|
|
205
|
+
description: "Using driver.manage().setImplicitWaitTimeout() or driver.sleep() instead of explicit waits. Implicit waits apply globally and interact badly with explicit waits. Fixed sleeps are arbitrary — too short causes flakiness, too long slows CI."
|
|
206
|
+
bad_example: |
|
|
207
|
+
// ❌ Global implicit wait — interacts badly with explicit waits
|
|
208
|
+
await driver.manage().setImplicitWaitTimeout(5000);
|
|
209
|
+
|
|
210
|
+
// ❌ Fixed sleep — arbitrary, slow on fast machines, still flaky on slow ones
|
|
211
|
+
await driver.sleep(3000);
|
|
212
|
+
const el = await driver.findElement(By.css('#result'));
|
|
213
|
+
good_example: |
|
|
214
|
+
// ✓ Explicit wait — resolves as soon as element appears, up to 5s
|
|
215
|
+
const el = await driver.wait(
|
|
216
|
+
until.elementLocated(By.css('[data-testid="result"]')),
|
|
217
|
+
5000,
|
|
218
|
+
'Result element not found within 5s'
|
|
219
|
+
);
|
|
220
|
+
- id: driver_not_quit
|
|
221
|
+
severity: critical
|
|
222
|
+
description: "Not calling driver.quit() after each test leaves orphaned Chrome/Firefox processes. Each test creates a new browser process — 20 tests without cleanup = 20 browser processes competing for memory. In CI, this causes OOM kills."
|
|
223
|
+
bad_example: |
|
|
224
|
+
// ❌ No cleanup — browser process leaked after every test
|
|
225
|
+
afterEach(async () => {
|
|
226
|
+
// Missing: await driver.quit();
|
|
227
|
+
});
|
|
228
|
+
good_example: |
|
|
229
|
+
// ✓ quit() in try/catch so cleanup always runs
|
|
230
|
+
afterEach(async () => {
|
|
231
|
+
try {
|
|
232
|
+
await driver.quit();
|
|
233
|
+
} catch {
|
|
234
|
+
// driver may already be closed if test crashed it
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
- id: new_builder_in_tests
|
|
238
|
+
severity: warning
|
|
239
|
+
description: "Calling `new Builder()` in individual test files instead of importing from the shared driver factory. Browser configuration (headless mode, window size, proxy) is duplicated and must be updated in every test file when it changes."
|
|
240
|
+
bad_example: |
|
|
241
|
+
// ❌ Builder configuration duplicated in every test file
|
|
242
|
+
beforeEach(async () => {
|
|
243
|
+
driver = await new Builder().forBrowser('chrome').build(); // headless? window size? proxy?
|
|
244
|
+
});
|
|
245
|
+
good_example: |
|
|
246
|
+
// ✓ Import from shared factory — one place to configure all browser options
|
|
247
|
+
import { createDriver } from '../fixtures/create-driver';
|
|
248
|
+
beforeEach(async () => { driver = await createDriver(); });
|
|
249
|
+
- id: xpath_selectors
|
|
250
|
+
severity: warning
|
|
251
|
+
description: "Using XPath selectors (By.xpath()) for element selection. XPath selectors are tightly coupled to the DOM tree structure — adding a wrapper <div>, moving elements, or renaming tags breaks the selector. They are also slow compared to CSS selectors."
|
|
252
|
+
bad_example: |
|
|
253
|
+
// ❌ XPath — breaks on any DOM structure change
|
|
254
|
+
await driver.findElement(By.xpath('//div[@class="login-form"]/form/div[2]/input')).sendKeys(email);
|
|
255
|
+
await driver.findElement(By.xpath('//button[contains(text(),"Submit")]')).click();
|
|
256
|
+
good_example: |
|
|
257
|
+
// ✓ data-testid selector — stable regardless of DOM structure
|
|
258
|
+
await driver.findElement(By.css('[data-testid="email-input"]')).sendKeys(email);
|
|
259
|
+
await driver.findElement(By.css('[data-testid="login-submit"]')).click();
|
|
260
|
+
- id: no_screenshot_on_failure
|
|
261
|
+
severity: warning
|
|
262
|
+
description: "Not capturing screenshots on test failure. A headless browser test that fails in CI with no screenshot leaves only an error message — developers cannot see what the page looked like when the test failed, making debugging extremely difficult."
|
|
263
|
+
bad_example: |
|
|
264
|
+
// ❌ No screenshot — only error message on failure
|
|
265
|
+
afterEach(async () => {
|
|
266
|
+
await driver.quit(); // no screenshot before quitting
|
|
267
|
+
});
|
|
268
|
+
good_example: |
|
|
269
|
+
// ✓ Screenshot saved before quit when test fails
|
|
270
|
+
afterEach(async function() {
|
|
271
|
+
if (this.currentTest?.state === 'failed') {
|
|
272
|
+
const screenshot = await driver.takeScreenshot();
|
|
273
|
+
fs.writeFileSync(`test-screenshots/${Date.now()}.png`, screenshot, 'base64');
|
|
274
|
+
}
|
|
275
|
+
await driver.quit();
|
|
276
|
+
});
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: supabase-auth
|
|
3
|
+
name: "Supabase Auth"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Supabase built-in authentication with SSR session management via @supabase/ssr, RLS row-level security policies tied to auth.uid(), OAuth callback routes, and middleware session refresh."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- supabase
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- "@clerk/nextjs"
|
|
13
|
+
- next-auth
|
|
14
|
+
- lucia
|
|
15
|
+
detection:
|
|
16
|
+
dependencies:
|
|
17
|
+
any:
|
|
18
|
+
- "@supabase/ssr"
|
|
19
|
+
- "@supabase/supabase-js"
|
|
20
|
+
source_indicators:
|
|
21
|
+
- "supabase.auth.signIn"
|
|
22
|
+
- "supabase.auth.signUp"
|
|
23
|
+
- "auth.uid()"
|
|
24
|
+
- "exchangeCodeForSession"
|
|
25
|
+
- "createServerClient"
|
|
26
|
+
- "supabase.auth.getUser"
|
|
27
|
+
structure:
|
|
28
|
+
required_dirs:
|
|
29
|
+
- path: app/auth
|
|
30
|
+
purpose: "Supabase auth UI routes and OAuth callback handler. The callback route at app/auth/callback/route.ts is mandatory for all OAuth providers — it exchanges the one-time code from Supabase's redirect URL for a session cookie. Without it, OAuth sign-in always fails."
|
|
31
|
+
recommended_dirs:
|
|
32
|
+
- path: src/lib
|
|
33
|
+
purpose: "Supabase client factory functions — one file for server contexts (createClient reading from Next.js cookies) and one for browser contexts (createBrowserClient). These files are imported by Server Components, API routes, middleware, and Client Components respectively. Never mix them."
|
|
34
|
+
- path: middleware.ts
|
|
35
|
+
purpose: "Session refresh middleware — calls supabase.auth.getUser() on every request to refresh the session cookie before it expires. Without this, users are silently logged out when their session cookie ages past the expiry window even if they are actively using the app."
|
|
36
|
+
separation:
|
|
37
|
+
rules:
|
|
38
|
+
- concern: ssr_sessions
|
|
39
|
+
belongs_in: src/lib
|
|
40
|
+
rule_text: "Use @supabase/ssr (not @supabase/supabase-js directly) for Next.js and server-rendered apps. Create a server client in src/lib/supabase-server.ts that reads and writes session cookies using Next.js cookie utilities. Create a browser client in src/lib/supabase-browser.ts for Client Components."
|
|
41
|
+
example: |
|
|
42
|
+
// src/lib/supabase-server.ts — for Server Components, API routes, middleware
|
|
43
|
+
import { createServerClient } from '@supabase/ssr';
|
|
44
|
+
import { cookies } from 'next/headers';
|
|
45
|
+
|
|
46
|
+
export async function createClient() {
|
|
47
|
+
const cookieStore = await cookies();
|
|
48
|
+
return createServerClient(
|
|
49
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
50
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
51
|
+
{
|
|
52
|
+
cookies: {
|
|
53
|
+
getAll: () => cookieStore.getAll(),
|
|
54
|
+
setAll: (cookiesToSet) => {
|
|
55
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
56
|
+
cookieStore.set(name, value, options)
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/lib/supabase-browser.ts — for Client Components only
|
|
65
|
+
import { createBrowserClient } from '@supabase/ssr';
|
|
66
|
+
export const supabase = createBrowserClient(
|
|
67
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
68
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
69
|
+
);
|
|
70
|
+
indicators:
|
|
71
|
+
- "createServerClient"
|
|
72
|
+
- "@supabase/ssr"
|
|
73
|
+
- "createBrowserClient"
|
|
74
|
+
- "exchangeCodeForSession"
|
|
75
|
+
- concern: middleware_refresh
|
|
76
|
+
belongs_in: middleware.ts
|
|
77
|
+
rule_text: "Call supabase.auth.getUser() in middleware on every request to refresh the session cookie. This keeps short-lived access tokens alive as long as the user is active. Without middleware refresh, sessions expire mid-visit even when the user is actively interacting with the app."
|
|
78
|
+
example: |
|
|
79
|
+
// middleware.ts
|
|
80
|
+
import { createServerClient } from '@supabase/ssr';
|
|
81
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
82
|
+
|
|
83
|
+
export async function middleware(request: NextRequest) {
|
|
84
|
+
let supabaseResponse = NextResponse.next({ request });
|
|
85
|
+
|
|
86
|
+
const supabase = createServerClient(
|
|
87
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
88
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
89
|
+
{
|
|
90
|
+
cookies: {
|
|
91
|
+
getAll: () => request.cookies.getAll(),
|
|
92
|
+
setAll: (cookiesToSet) => {
|
|
93
|
+
cookiesToSet.forEach(({ name, value, options }) => {
|
|
94
|
+
request.cookies.set(name, value);
|
|
95
|
+
supabaseResponse.cookies.set(name, value, options);
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Refreshes the session if expired — MUST NOT be removed
|
|
103
|
+
const { data: { user } } = await supabase.auth.getUser();
|
|
104
|
+
|
|
105
|
+
if (!user && !request.nextUrl.pathname.startsWith('/login')) {
|
|
106
|
+
const url = request.nextUrl.clone();
|
|
107
|
+
url.pathname = '/login';
|
|
108
|
+
return NextResponse.redirect(url);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return supabaseResponse;
|
|
112
|
+
}
|
|
113
|
+
indicators:
|
|
114
|
+
- "supabase.auth.getUser()"
|
|
115
|
+
- "createServerClient"
|
|
116
|
+
- "supabaseResponse"
|
|
117
|
+
- concern: rls_integration
|
|
118
|
+
belongs_in: src/lib
|
|
119
|
+
rule_text: "Every table that stores user data must have Row Level Security enabled and at least one policy using auth.uid(). Supabase Auth's primary security mechanism is RLS — authenticated queries still see all rows if no policy restricts them. Write policies before writing application code."
|
|
120
|
+
example: |
|
|
121
|
+
-- Enable RLS on every user-data table (run in Supabase SQL editor)
|
|
122
|
+
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
|
123
|
+
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
|
|
124
|
+
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
125
|
+
|
|
126
|
+
-- Policy: users can only read, create, update, and delete their own rows
|
|
127
|
+
CREATE POLICY "own posts" ON posts
|
|
128
|
+
FOR ALL
|
|
129
|
+
USING (auth.uid() = user_id)
|
|
130
|
+
WITH CHECK (auth.uid() = user_id);
|
|
131
|
+
|
|
132
|
+
-- Policy: profiles are readable by everyone but only editable by owner
|
|
133
|
+
CREATE POLICY "public profiles read" ON profiles
|
|
134
|
+
FOR SELECT USING (true);
|
|
135
|
+
CREATE POLICY "own profile write" ON profiles
|
|
136
|
+
FOR ALL USING (auth.uid() = id);
|
|
137
|
+
indicators:
|
|
138
|
+
- "auth.uid()"
|
|
139
|
+
- "USING ("
|
|
140
|
+
- "CREATE POLICY"
|
|
141
|
+
- "ROW LEVEL SECURITY"
|
|
142
|
+
- concern: social_providers
|
|
143
|
+
belongs_in: app/auth
|
|
144
|
+
rule_text: "Configure OAuth providers in the Supabase dashboard (Authentication > Providers) — not in code. Create a mandatory callback route at app/auth/callback/route.ts that calls supabase.auth.exchangeCodeForSession() to convert the one-time OAuth code into a session cookie."
|
|
145
|
+
example: |
|
|
146
|
+
// app/auth/callback/route.ts — mandatory for all OAuth providers
|
|
147
|
+
import { createServerClient } from '@supabase/ssr';
|
|
148
|
+
import { cookies } from 'next/headers';
|
|
149
|
+
import { NextResponse } from 'next/server';
|
|
150
|
+
|
|
151
|
+
export async function GET(request: Request) {
|
|
152
|
+
const { searchParams, origin } = new URL(request.url);
|
|
153
|
+
const code = searchParams.get('code');
|
|
154
|
+
const next = searchParams.get('next') ?? '/';
|
|
155
|
+
|
|
156
|
+
if (code) {
|
|
157
|
+
const cookieStore = await cookies();
|
|
158
|
+
const supabase = createServerClient(
|
|
159
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
160
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
161
|
+
{ cookies: { getAll: () => cookieStore.getAll(), setAll: (cs) => cs.forEach(({ name, value, options }) => cookieStore.set(name, value, options)) } }
|
|
162
|
+
);
|
|
163
|
+
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
164
|
+
if (!error) {
|
|
165
|
+
return NextResponse.redirect(`${origin}${next}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
|
|
170
|
+
}
|
|
171
|
+
indicators:
|
|
172
|
+
- "exchangeCodeForSession"
|
|
173
|
+
- "app/auth/callback"
|
|
174
|
+
- "searchParams.get('code')"
|
|
175
|
+
- concern: server_component_auth
|
|
176
|
+
belongs_in: app
|
|
177
|
+
rule_text: "In Server Components, call supabase.auth.getUser() (not getSession()) to get the authenticated user. getUser() validates the token with Supabase's server — getSession() only reads the local cookie which can be forged. Always use getUser() for security-sensitive decisions."
|
|
178
|
+
example: |
|
|
179
|
+
// app/dashboard/page.tsx — Server Component with auth check
|
|
180
|
+
import { createClient } from '@/lib/supabase-server';
|
|
181
|
+
import { redirect } from 'next/navigation';
|
|
182
|
+
|
|
183
|
+
export default async function DashboardPage() {
|
|
184
|
+
const supabase = await createClient();
|
|
185
|
+
// getUser() validates with Supabase server — getSession() does NOT
|
|
186
|
+
const { data: { user }, error } = await supabase.auth.getUser();
|
|
187
|
+
if (error || !user) redirect('/login');
|
|
188
|
+
|
|
189
|
+
const { data: posts } = await supabase
|
|
190
|
+
.from('posts')
|
|
191
|
+
.select('*')
|
|
192
|
+
.eq('user_id', user.id); // RLS also enforces this at DB level
|
|
193
|
+
|
|
194
|
+
return <PostList posts={posts ?? []} />;
|
|
195
|
+
}
|
|
196
|
+
indicators:
|
|
197
|
+
- "supabase.auth.getUser()"
|
|
198
|
+
- "from('@/lib/supabase-server')"
|
|
199
|
+
patterns:
|
|
200
|
+
data_flow:
|
|
201
|
+
direction: "Request → Middleware (session refresh) → Server Component (getUser) → RLS-enforced Supabase Query → Database"
|
|
202
|
+
rules:
|
|
203
|
+
- "Middleware refreshes the session cookie on every request — without it users get silently logged out mid-session."
|
|
204
|
+
- "Server Components call supabase.auth.getUser() for the authenticated user — never getSession() for security decisions."
|
|
205
|
+
- "Client Components use the browser client from supabase-browser.ts — it reads the session from localStorage."
|
|
206
|
+
- "RLS policies enforce per-user data isolation at the database level — authenticated queries still need policies to restrict rows."
|
|
207
|
+
- "The OAuth callback route at app/auth/callback/route.ts is the mandatory landing point for all social provider sign-ins."
|
|
208
|
+
- "Service role key is only used server-side for admin operations that intentionally bypass RLS — never in browser code."
|
|
209
|
+
error_handling:
|
|
210
|
+
recommended: "Always call supabase.auth.getUser() (not getSession()) for auth decisions — getUser() validates with Supabase's server. Check both error and data.user being null. Redirect to /login when no valid session."
|
|
211
|
+
naming:
|
|
212
|
+
callback_route: "app/auth/callback/route.ts — OAuth code exchange via exchangeCodeForSession()"
|
|
213
|
+
server_client: "src/lib/supabase-server.ts — SSR client reading session from cookies (@supabase/ssr)"
|
|
214
|
+
browser_client: "src/lib/supabase-browser.ts — browser client (createBrowserClient from @supabase/ssr)"
|
|
215
|
+
rls_policies: "Named 'own [resource]' — e.g. 'own posts', 'own profiles'; reference auth.uid() = user_id"
|
|
216
|
+
anti_patterns:
|
|
217
|
+
- id: no_rls_with_auth
|
|
218
|
+
severity: critical
|
|
219
|
+
description: "Using Supabase Auth to authenticate users but not creating RLS policies. All authenticated users can read and modify each other's data — enabling auth without RLS does not provide any data isolation. This is the most common Supabase security mistake."
|
|
220
|
+
bad_example: |
|
|
221
|
+
-- ❌ RLS enabled but no policies — Supabase defaults to DENY for anon,
|
|
222
|
+
-- but ALL authenticated users can read ALL rows from each other
|
|
223
|
+
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
|
224
|
+
-- Missing: CREATE POLICY
|
|
225
|
+
-- Any signed-in user: SELECT * FROM posts → sees every user's posts
|
|
226
|
+
good_example: |
|
|
227
|
+
-- ✓ RLS with per-user policy — each user only sees their own rows
|
|
228
|
+
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
|
|
229
|
+
CREATE POLICY "own posts" ON posts
|
|
230
|
+
FOR ALL
|
|
231
|
+
USING (auth.uid() = user_id)
|
|
232
|
+
WITH CHECK (auth.uid() = user_id);
|
|
233
|
+
- id: get_session_for_auth_decisions
|
|
234
|
+
severity: critical
|
|
235
|
+
description: "Using supabase.auth.getSession() to verify authentication instead of supabase.auth.getUser(). getSession() reads the local session cookie without validating it with Supabase's server — a forged or replayed cookie will pass getSession() but fail getUser()."
|
|
236
|
+
bad_example: |
|
|
237
|
+
// ❌ getSession() only reads the cookie — can be forged
|
|
238
|
+
const { data: { session } } = await supabase.auth.getSession();
|
|
239
|
+
if (!session) redirect('/login');
|
|
240
|
+
// session.user is from the unvalidated cookie — not trustworthy
|
|
241
|
+
const userId = session.user.id;
|
|
242
|
+
good_example: |
|
|
243
|
+
// ✓ getUser() validates with Supabase's auth server — cannot be forged
|
|
244
|
+
const { data: { user }, error } = await supabase.auth.getUser();
|
|
245
|
+
if (error || !user) redirect('/login');
|
|
246
|
+
const userId = user.id; // validated server-side
|
|
247
|
+
- id: direct_supabase_js_on_server
|
|
248
|
+
severity: warning
|
|
249
|
+
description: "Using @supabase/supabase-js createClient() in Server Components instead of @supabase/ssr createServerClient(). The plain createClient() has no way to read Next.js cookie storage — it cannot find the user session and all authenticated queries run as anonymous."
|
|
250
|
+
bad_example: |
|
|
251
|
+
// ❌ Plain supabase-js on server — can't read session cookies
|
|
252
|
+
import { createClient } from '@supabase/supabase-js';
|
|
253
|
+
const supabase = createClient(URL, ANON_KEY);
|
|
254
|
+
// supabase.auth.getUser() always returns null — session is invisible
|
|
255
|
+
const { data: { user } } = await supabase.auth.getUser(); // user = null
|
|
256
|
+
good_example: |
|
|
257
|
+
// ✓ SSR client reads session from Next.js cookie store
|
|
258
|
+
import { createClient } from '@/lib/supabase-server';
|
|
259
|
+
const supabase = await createClient();
|
|
260
|
+
const { data: { user } } = await supabase.auth.getUser(); // user = authenticated user
|
|
261
|
+
- id: missing_callback_route
|
|
262
|
+
severity: warning
|
|
263
|
+
description: "Using OAuth social login (GitHub, Google, etc.) without an auth callback route. When Supabase redirects back after OAuth, there is nowhere to exchange the one-time code for a session — the user lands on the redirect URL with a 404 or a broken page."
|
|
264
|
+
bad_example: |
|
|
265
|
+
// ❌ No callback route — sign-in with OAuth always fails with a broken redirect
|
|
266
|
+
await supabase.auth.signInWithOAuth({ provider: 'github' });
|
|
267
|
+
// Supabase redirects to: /auth/callback?code=... → 404 Not Found
|
|
268
|
+
good_example: |
|
|
269
|
+
// ✓ app/auth/callback/route.ts exists and calls exchangeCodeForSession()
|
|
270
|
+
const { error } = await supabase.auth.exchangeCodeForSession(code);
|
|
271
|
+
if (!error) return NextResponse.redirect(`${origin}/dashboard`);
|
|
272
|
+
- id: missing_middleware_refresh
|
|
273
|
+
severity: warning
|
|
274
|
+
description: "Not running session refresh in middleware. Supabase access tokens expire after 1 hour by default. Without middleware calling getUser() on each request, the session cookie ages and the user is silently logged out even while actively using the app."
|
|
275
|
+
bad_example: |
|
|
276
|
+
// middleware.ts — missing or empty, no session refresh
|
|
277
|
+
export function middleware(req: NextRequest) {
|
|
278
|
+
return NextResponse.next(); // session cookie never refreshed
|
|
279
|
+
}
|
|
280
|
+
// User gets logged out every hour even while actively using the app
|
|
281
|
+
good_example: |
|
|
282
|
+
// ✓ Middleware calls supabase.auth.getUser() which refreshes the access token
|
|
283
|
+
const supabase = createServerClient(URL, KEY, { cookies: { ... } });
|
|
284
|
+
await supabase.auth.getUser(); // side effect: refreshes session if near expiry
|
|
285
|
+
- id: insecure_rls_policy
|
|
286
|
+
severity: critical
|
|
287
|
+
description: "Writing an RLS policy with USING (true) which grants full access to all authenticated users — equivalent to having no RLS at all. Often introduced by accident when following generic Supabase examples."
|
|
288
|
+
bad_example: |
|
|
289
|
+
-- ❌ USING (true) grants every authenticated user full table access
|
|
290
|
+
CREATE POLICY "allow all" ON posts
|
|
291
|
+
FOR ALL USING (true);
|
|
292
|
+
-- Any logged-in user can read, update, or delete any other user's posts
|
|
293
|
+
good_example: |
|
|
294
|
+
-- ✓ Scope the policy to the authenticated user's own rows
|
|
295
|
+
CREATE POLICY "own posts" ON posts
|
|
296
|
+
FOR ALL
|
|
297
|
+
USING (auth.uid() = user_id)
|
|
298
|
+
WITH CHECK (auth.uid() = user_id);
|