@everystack/mcp 0.2.3 → 0.3.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.
Files changed (43) hide show
  1. package/README.md +37 -10
  2. package/dist/adding-database.md +169 -0
  3. package/dist/admin.md +81 -0
  4. package/dist/auth.md +115 -0
  5. package/dist/aws-setup.md +276 -0
  6. package/dist/cli.md +108 -0
  7. package/dist/client-api.md +145 -0
  8. package/dist/core.md +196 -0
  9. package/dist/deployment.md +146 -0
  10. package/dist/events.md +87 -0
  11. package/dist/first-run.md +100 -0
  12. package/dist/getting-started.md +75 -0
  13. package/dist/handler-options.md +114 -0
  14. package/dist/images.md +93 -0
  15. package/dist/index.cjs +23726 -0
  16. package/dist/jobs.md +97 -0
  17. package/dist/logging.md +91 -0
  18. package/dist/plugins.md +68 -0
  19. package/dist/project-claude-md.md +102 -0
  20. package/dist/query-protocol.md +129 -0
  21. package/dist/schema-patterns.md +167 -0
  22. package/dist/security-device.md +99 -0
  23. package/dist/security.md +270 -0
  24. package/dist/ssr.md +82 -0
  25. package/dist/storage.md +63 -0
  26. package/dist/testing.md +118 -0
  27. package/package.json +11 -9
  28. package/src/gates/detectors/embedded-data-bundle.ts +58 -0
  29. package/src/gates/detectors/hand-written-migration.ts +42 -0
  30. package/src/gates/detectors/secret-in-public-env.ts +41 -0
  31. package/src/gates/engine.ts +80 -0
  32. package/src/gates/registry.ts +25 -0
  33. package/src/gates/telemetry.ts +143 -0
  34. package/src/gates/types.ts +70 -0
  35. package/src/governance/cli.ts +193 -0
  36. package/src/governance/grounding.ts +344 -0
  37. package/src/index.ts +97 -50
  38. package/src/prompts/claude-md.ts +90 -0
  39. package/src/prompts/governance-setup.ts +85 -0
  40. package/src/prompts/index.ts +4 -0
  41. package/src/prompts/new-app.ts +4 -1
  42. package/src/resources/project-claude-md.md +69 -94
  43. package/src/tools/index.ts +6 -39
@@ -0,0 +1,99 @@
1
+ # Device Security
2
+
3
+ > Device attestation and biometric auth. Import from `@everystack/security`.
4
+
5
+ ## When to Use
6
+ Read this when adding hardware-backed security: device trust verification, biometric authentication, or certificate pinning.
7
+
8
+ ## Setup
9
+
10
+ ```typescript
11
+ import { createSecurityHandler } from '@everystack/security';
12
+
13
+ const security = createSecurityHandler({
14
+ apple: { teamId: '...', bundleId: '...' },
15
+ google: { packageName: '...' },
16
+ });
17
+ ```
18
+
19
+ ## Device Attestation
20
+
21
+ Verify that requests come from genuine devices (not emulators or modified apps).
22
+
23
+ ### Apple App Attest
24
+ ```typescript
25
+ import { verifyAppleAttestation } from '@everystack/security';
26
+
27
+ const result = await verifyAppleAttestation({
28
+ attestation: base64AttestationData,
29
+ challenge: serverChallenge,
30
+ teamId: 'TEAM_ID',
31
+ bundleId: 'com.example.app',
32
+ });
33
+ ```
34
+
35
+ ### Google Play Integrity
36
+ ```typescript
37
+ import { verifyPlayIntegrity } from '@everystack/security';
38
+
39
+ const result = await verifyPlayIntegrity({
40
+ token: integrityToken,
41
+ packageName: 'com.example.app',
42
+ });
43
+ ```
44
+
45
+ ## RS256 Device Keys
46
+
47
+ Per-device RSA key pairs for request signing:
48
+
49
+ ```typescript
50
+ import { generateDeviceKey, signRequest, verifySignature } from '@everystack/security/crypto';
51
+
52
+ // Client: generate and store key pair
53
+ const { publicKey, privateKey } = await generateDeviceKey();
54
+
55
+ // Client: sign requests
56
+ const signature = await signRequest(privateKey, requestBody);
57
+
58
+ // Server: verify signature
59
+ const valid = await verifySignature(publicKey, requestBody, signature);
60
+ ```
61
+
62
+ ## Auth Plugin Integration
63
+
64
+ ```typescript
65
+ import { authPlugin } from '@everystack/auth/plugin';
66
+
67
+ authPlugin({
68
+ device: {
69
+ verify: async (attestation, claims) => {
70
+ // Verify device attestation
71
+ return true; // or false to reject
72
+ },
73
+ require: 'always', // or 'optional'
74
+ },
75
+ });
76
+ ```
77
+
78
+ ## Schema
79
+
80
+ ```typescript
81
+ import { securitySchema } from '@everystack/security/schema';
82
+ // Adds: device_attestations, device_keys tables
83
+ ```
84
+
85
+ ## Client SDK
86
+
87
+ ```typescript
88
+ import { createSecurityClient } from '@everystack/security/client';
89
+
90
+ const security = createSecurityClient({ baseUrl: '/api/security' });
91
+ await security.registerDevice({ publicKey, attestation });
92
+ ```
93
+
94
+ ## Gotchas
95
+
96
+ - Apple App Attest requires iOS 14+ and a real device (not simulator)
97
+ - Google Play Integrity requires Google Play Services
98
+ - Device keys should be stored in the device's secure enclave/keystore
99
+ - Attestation verification should be done server-side only
@@ -0,0 +1,270 @@
1
+ # everystack Security
2
+
3
+ Defense-in-depth across three independent layers. Each layer is a complete security boundary. An attacker must bypass all three.
4
+
5
+ ## Three-Layer Model
6
+
7
+ ```
8
+ Request
9
+ -> Edge (CloudFront) JWT valid? -> 401 if invalid
10
+ -> Handler (Lambda) SET LOCAL ROLE + inject -> 401/403
11
+ -> PostgreSQL (RLS) GRANT + RLS policies -> 403 if denied
12
+ ```
13
+
14
+ 1. **Edge (CloudFront Functions):** HS256 JWT signature + expiry check. Invalid tokens never reach Lambda. Constant-time comparison prevents timing attacks.
15
+ 2. **Handler (Lambda):** JWT verification, `SET LOCAL ROLE` via pgSettings, RPC role gates, rowOwnership, exposedTables/hiddenColumns/protectedFields.
16
+ 3. **Database (PostgreSQL):** GRANTs (table-level) + RLS policies (row-level). The single source of truth for authorization. Even if the handler has a bug, the database enforces access.
17
+
18
+ ## AWS IAM Credential Profiles
19
+
20
+ Three named profiles prevent full account takeover. Configure in `~/.aws/config`:
21
+
22
+ ### everystack-create (infrastructure provisioning)
23
+ ```ini
24
+ [profile everystack-create]
25
+ region = us-east-1
26
+ ```
27
+ **Permissions:** CloudFormation, RDS, VPC, IAM role creation, Route53, SQS, S3 bucket creation, Lambda function creation.
28
+ **When to use:** Initial `sst deploy` and when adding new infrastructure (new queue, new bucket).
29
+ **DISABLE after initial deployment.** Re-enable temporarily when infrastructure changes are needed. This profile can create IAM roles, which is the most dangerous permission.
30
+
31
+ ### everystack-manage (day-to-day operations)
32
+ ```ini
33
+ [profile everystack-manage]
34
+ region = us-east-1
35
+ ```
36
+ **Permissions:** Lambda invoke, S3 read/write, CloudWatch logs, SSM parameters, CloudFront KVS.
37
+ **When to use:** All CLI commands: `AWS_PROFILE=everystack-manage everystack db:migrate`, `everystack logs:tail`, `everystack update`, `everystack console`.
38
+ **Cannot** create or destroy infrastructure, modify IAM, or change VPC/RDS configuration.
39
+
40
+ ### everystack-deploy (CI/CD)
41
+ ```ini
42
+ [profile everystack-deploy]
43
+ region = us-east-1
44
+ ```
45
+ **Permissions:** Lambda update-function-code, CloudFront invalidation, S3 sync (deploy buckets only).
46
+ **When to use:** GitHub Actions deploys. Can push code, cannot change infrastructure shape.
47
+ **Cannot** read secrets, invoke Lambda, or access CloudWatch logs.
48
+
49
+ ### Hard boundary
50
+ None of the three profiles can: modify IAM policies, assume other roles, access other AWS accounts, or create new IAM users/keys.
51
+
52
+ ## Row-Level Security (RLS)
53
+
54
+ RLS is PostgreSQL's built-in mechanism for restricting which rows a query can see or modify. everystack makes RLS work automatically through pgSettings.
55
+
56
+ ### Mental Model
57
+
58
+ 1. Handler receives JWT, extracts claims (user ID, role)
59
+ 2. Handler calls `SET LOCAL ROLE authenticated` inside a transaction
60
+ 3. Handler calls `set_config('request.jwt.claims', '{"sub":"user-123","role":"authenticated"}', true)`
61
+ 4. PostgreSQL evaluates RLS policies using the role and claims
62
+ 5. Transaction ends, role and settings reset (no leakage across requests)
63
+
64
+ ### Role Architecture
65
+
66
+ ```sql
67
+ -- Login role (NOINHERIT -- has no privileges of its own)
68
+ CREATE ROLE authenticator LOGIN NOINHERIT;
69
+
70
+ -- Application roles (NOLOGIN -- can't connect directly)
71
+ CREATE ROLE anon NOLOGIN;
72
+ CREATE ROLE authenticated NOLOGIN;
73
+ CREATE ROLE admin NOLOGIN;
74
+
75
+ -- Grant chain: authenticator can become any role via SET LOCAL ROLE
76
+ GRANT anon TO authenticator;
77
+ GRANT authenticated TO authenticator;
78
+ GRANT admin TO authenticator;
79
+ ```
80
+
81
+ Why `NOINHERIT`: authenticator has zero privileges. All access comes through the granted roles. This prevents accidental privilege leakage.
82
+
83
+ ### Table Grants
84
+
85
+ ```sql
86
+ GRANT USAGE ON SCHEMA public TO anon, authenticated, admin;
87
+
88
+ -- anon: read-only (public data)
89
+ GRANT SELECT ON posts, profiles TO anon;
90
+
91
+ -- authenticated: read + write (own data, enforced by RLS)
92
+ GRANT SELECT, INSERT, UPDATE, DELETE ON posts, profiles TO authenticated;
93
+
94
+ -- admin: full access
95
+ GRANT SELECT, INSERT, UPDATE, DELETE ON posts, profiles TO admin;
96
+ ```
97
+
98
+ ### Enable RLS
99
+
100
+ ```sql
101
+ ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
102
+ ALTER TABLE posts FORCE ROW LEVEL SECURITY;
103
+ ```
104
+
105
+ `FORCE` ensures RLS applies even to the table owner. Without it, the role that created the table bypasses all policies silently.
106
+
107
+ ### Policy Templates
108
+
109
+ Copy-paste these patterns. Replace `posts` with your table name and `author_id` with your ownership column.
110
+
111
+ **Anon read (public data):**
112
+ ```sql
113
+ CREATE POLICY posts_select_anon ON posts
114
+ FOR SELECT TO anon
115
+ USING (deleted_at IS NULL);
116
+ ```
117
+
118
+ **Authenticated read (all non-deleted):**
119
+ ```sql
120
+ CREATE POLICY posts_select_authenticated ON posts
121
+ FOR SELECT TO authenticated
122
+ USING (deleted_at IS NULL);
123
+ ```
124
+
125
+ **Authenticated insert (own rows only):**
126
+ ```sql
127
+ CREATE POLICY posts_insert_own ON posts
128
+ FOR INSERT TO authenticated
129
+ WITH CHECK (
130
+ author_id = (current_setting('request.jwt.claims', true)::jsonb->>'sub')::uuid
131
+ );
132
+ ```
133
+
134
+ **Authenticated update (own rows only):**
135
+ ```sql
136
+ CREATE POLICY posts_update_own ON posts
137
+ FOR UPDATE TO authenticated
138
+ USING (author_id = (current_setting('request.jwt.claims', true)::jsonb->>'sub')::uuid)
139
+ WITH CHECK (author_id = (current_setting('request.jwt.claims', true)::jsonb->>'sub')::uuid);
140
+ ```
141
+
142
+ **Authenticated delete (own rows only):**
143
+ ```sql
144
+ CREATE POLICY posts_delete_own ON posts
145
+ FOR DELETE TO authenticated
146
+ USING (author_id = (current_setting('request.jwt.claims', true)::jsonb->>'sub')::uuid);
147
+ ```
148
+
149
+ **Admin full access:**
150
+ ```sql
151
+ CREATE POLICY posts_admin ON posts
152
+ FOR ALL TO admin
153
+ USING (true)
154
+ WITH CHECK (true);
155
+ ```
156
+
157
+ The `current_setting('request.jwt.claims', true)::jsonb->>'sub'` pattern reads the JWT payload injected by the handler via pgSettings. The `true` parameter returns NULL instead of raising an error when the setting doesn't exist.
158
+
159
+ ### Handler Configuration for RLS
160
+
161
+ ```typescript
162
+ createHandler(db, schema, {
163
+ auth: {
164
+ verifyToken: async (token) => verifyJwt(token),
165
+ publicRoutes: ['GET'],
166
+ },
167
+ pgSettings: (user) => ({
168
+ role: user?.role === 'admin' ? 'admin' : user ? 'authenticated' : 'anon',
169
+ 'request.jwt.claims': JSON.stringify(user || { role: 'anon' }),
170
+ }),
171
+ });
172
+ ```
173
+
174
+ The `role` key triggers `SET LOCAL ROLE <value>`. The role name is validated against `^[a-zA-Z_][a-zA-Z0-9_]*$` to prevent injection.
175
+
176
+ ## SECURITY DEFINER vs SECURITY INVOKER
177
+
178
+ PostgreSQL functions default to SECURITY INVOKER (runs as the current role, RLS applies). SECURITY DEFINER runs as the function owner, bypassing RLS.
179
+
180
+ **Use SECURITY INVOKER (default)** for read-only functions (list, get, search). RLS policies apply normally.
181
+
182
+ **Use SECURITY DEFINER** only for mutations needing privilege escalation (stats aggregation, cross-user operations). Always:
183
+ 1. `REVOKE EXECUTE FROM PUBLIC` on the function
184
+ 2. Set `search_path` explicitly to prevent hijacking
185
+ 3. Document why DEFINER is needed
186
+
187
+ ```sql
188
+ CREATE FUNCTION aggregate_stats()
189
+ RETURNS TABLE(total bigint)
190
+ SECURITY DEFINER
191
+ SET search_path = public, pg_temp
192
+ AS $$ SELECT count(*) FROM posts; $$
193
+ LANGUAGE sql;
194
+
195
+ REVOKE EXECUTE ON FUNCTION aggregate_stats() FROM PUBLIC;
196
+ GRANT EXECUTE ON FUNCTION aggregate_stats() TO admin;
197
+ ```
198
+
199
+ ## IDOR Prevention
200
+
201
+ Two layers working together:
202
+
203
+ **Handler layer (rowOwnership):**
204
+ ```typescript
205
+ rowOwnership: {
206
+ posts: { column: 'authorId', userField: 'sub' },
207
+ }
208
+ ```
209
+ Effect: PATCH and DELETE auto-append `WHERE authorId = user.sub`. Returns 404 if row doesn't belong to user.
210
+
211
+ **Database layer (RLS):** Policies enforce the same constraint at SQL level. Even if the handler is bypassed (e.g., SSR direct query), the database enforces ownership.
212
+
213
+ Use both. rowOwnership catches IDOR at HTTP level with clear 404. RLS catches it at SQL level as a safety net.
214
+
215
+ ## Testing Checklist
216
+
217
+ Before deploying RLS:
218
+
219
+ ```sql
220
+ -- As authenticator (the connection role)
221
+ SET ROLE authenticated;
222
+ SET LOCAL request.jwt.claims = '{"sub":"user-123","role":"authenticated"}';
223
+
224
+ -- Verify: only user-123's posts visible
225
+ SELECT * FROM posts;
226
+
227
+ -- Verify: can create own post
228
+ INSERT INTO posts (body, author_id) VALUES ('test', 'user-123');
229
+
230
+ -- Verify: cannot create post as another user
231
+ INSERT INTO posts (body, author_id) VALUES ('test', 'user-456'); -- should fail
232
+
233
+ -- Verify: admin sees everything
234
+ SET ROLE admin;
235
+ SELECT * FROM posts;
236
+
237
+ RESET ROLE;
238
+ ```
239
+
240
+ ## Deployment Checklist
241
+
242
+ ### Database
243
+ - [ ] Create `authenticator`, `anon`, `authenticated`, `admin` roles
244
+ - [ ] Grant chain: authenticator can SET ROLE to each
245
+ - [ ] GRANT table-level permissions per role
246
+ - [ ] ENABLE + FORCE ROW LEVEL SECURITY on all public tables
247
+ - [ ] Write RLS policies per table x role
248
+ - [ ] Connect as `authenticator` (never use RDS master/superuser in production)
249
+ - [ ] Test: SET ROLE authenticated; SELECT returns only expected rows
250
+
251
+ ### Handler
252
+ - [ ] `exposedTables` whitelist (404 for unlisted tables)
253
+ - [ ] `hiddenColumns` for password hashes, tokens, internal IDs
254
+ - [ ] `protectedFields` for role, status, admin-only fields
255
+ - [ ] `rowOwnership` scoping writes to authenticated user
256
+ - [ ] `pgSettings` injecting role + request.jwt.claims
257
+ - [ ] `maxEmbedDepth: 3` and `maxLimit: 1000`
258
+
259
+ ### Auth
260
+ - [ ] JWT secret in SST secrets (not in code or env vars)
261
+ - [ ] Access tokens short-lived (15 min for edge, 1h otherwise)
262
+ - [ ] DB-backed refresh tokens with one-time-use rotation
263
+ - [ ] Bcrypt cost >= 10
264
+
265
+ ### Infrastructure
266
+ - [ ] WAF managed rules (exclude SQLi_QUERYARGUMENTS on API paths for PostgREST syntax)
267
+ - [ ] Rate limiting (1000 req/5 min per IP)
268
+ - [ ] CloudFront edge JWT verification for latency-sensitive paths
269
+ - [ ] Database credentials via SST Resource linking
270
+ - [ ] No debug endpoints or env dumps in production
package/dist/ssr.md ADDED
@@ -0,0 +1,82 @@
1
+ # Server-Side Rendering
2
+
3
+ > SSR with direct database access for everystack Expo apps. Import from `@everystack/server/ssr`.
4
+
5
+ ## When to Use
6
+ Read this when building server-rendered pages for SEO, social sharing, or initial load performance.
7
+
8
+ ## Setup
9
+
10
+ ```typescript
11
+ import { getWebHandler } from '@everystack/server/ssr';
12
+
13
+ const webHandler = getWebHandler({
14
+ channel: process.env.ENVIRONMENT || 'production',
15
+ bucket: Resource.Updates.name,
16
+ });
17
+ ```
18
+
19
+ ## How SSR Works
20
+
21
+ 1. Browser requests a page (e.g., `/alice`)
22
+ 2. Expo Router matches `app/[username]/index.tsx`
23
+ 3. The `loader()` function runs server-side
24
+ 4. Loader queries database directly via Drizzle (zero network overhead)
25
+ 5. Component renders with data, `<Head>` injects OG meta + JSON-LD
26
+ 6. HTML sent to browser with full SEO markup
27
+
28
+ ## Loader Pattern
29
+
30
+ ```typescript
31
+ // app/[username]/index.tsx
32
+ export async function loader({ params }) {
33
+ const profile = await db.query.profiles.findFirst({
34
+ where: eq(profiles.username, params.username),
35
+ });
36
+ const postCount = await db.select({ count: count() }).from(posts)
37
+ .where(eq(posts.authorId, profile.id));
38
+
39
+ return { profile, postCount: Number(postCount[0].count) };
40
+ }
41
+
42
+ export default function ProfilePage() {
43
+ const { profile, postCount } = useLoaderData();
44
+ return (
45
+ <>
46
+ <Head>
47
+ <title>{profile.displayName}</title>
48
+ <meta property="og:title" content={profile.displayName} />
49
+ </Head>
50
+ <ProfileView profile={profile} postCount={postCount} />
51
+ </>
52
+ );
53
+ }
54
+ ```
55
+
56
+ ## SSR Best Practices
57
+
58
+ **Do:**
59
+ - Return absolute timestamps from loaders (ISO strings)
60
+ - Use `timeZone: 'UTC'` in `toLocaleDateString()` calls
61
+ - Coerce SQL aggregates to numbers in the loader (`Number(count)`)
62
+ - Use `<Head>` for OG meta and JSON-LD structured data
63
+
64
+ **Don't:**
65
+ - Render relative time ("5 minutes ago") in loaders (gets cached for days)
66
+ - Use `useEffect(() => fetchData(), [])` when loader data is available
67
+ - Forget `timeZone: 'UTC'` (server and client render different dates)
68
+
69
+ ## Debugging
70
+
71
+ ```bash
72
+ everystack diag https://myapp.com/page --hydration # Runtime analysis
73
+ everystack analyze:ssr # Static code analysis
74
+ ```
75
+
76
+ ## Gotchas
77
+
78
+ - SSR loaders query the database directly (no HTTP, no auth overhead)
79
+ - SSR responses are cached by CloudFront (check `Cache-Control` headers)
80
+ - Relative time in SSR HTML becomes stale in cache
81
+ - SQL aggregates return strings after JSON round-trip (coerce with `Number()`)
82
+ - Both SSR and API use the same Drizzle schema (add column once, both paths see it)
@@ -0,0 +1,63 @@
1
+ # File Storage
2
+
3
+ > S3 file uploads with presigned URLs and CDN delivery. Import from `@everystack/storage`.
4
+
5
+ ## When to Use
6
+ Read this when adding file uploads (V3): profile pictures, attachments, media.
7
+
8
+ ## Setup
9
+
10
+ ```typescript
11
+ import { createStorageHandler } from '@everystack/storage';
12
+
13
+ const storage = createStorageHandler({
14
+ bucket: Resource.Media.name,
15
+ maxSize: 10 * 1024 * 1024, // 10MB
16
+ allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
17
+ auth: { verifyToken: auth.verifyToken },
18
+ presignedUrlExpiry: 3600, // 1 hour
19
+ });
20
+ ```
21
+
22
+ ## Upload Flow
23
+
24
+ 1. Client requests presigned upload URL: `POST /api/storage/upload`
25
+ 2. Server generates presigned S3 PUT URL with MIME validation
26
+ 3. Client uploads directly to S3 (no Lambda in the upload path)
27
+ 4. Client confirms upload: `POST /api/storage/confirm`
28
+ 5. Server validates the object exists and records metadata
29
+
30
+ ## Client SDK
31
+
32
+ ```typescript
33
+ import { createStorageClient } from '@everystack/storage/client';
34
+
35
+ const storage = createStorageClient({ baseUrl: '/api/storage' });
36
+
37
+ // Upload a file
38
+ const { url, key } = await storage.upload(file, {
39
+ contentType: 'image/jpeg',
40
+ metadata: { userId: user.sub },
41
+ });
42
+ ```
43
+
44
+ ## CDN Delivery
45
+
46
+ Files are served via CloudFront. The image handler (`@everystack/images`) can resize on-the-fly:
47
+ ```
48
+ /media/photo.jpg?w=400&h=300&fit=cover&fm=webp&q=80
49
+ ```
50
+
51
+ ## Plugin
52
+
53
+ ```typescript
54
+ import { storagePlugin } from '@everystack/storage/plugin';
55
+ // Adds upload, confirm, list, delete routes
56
+ ```
57
+
58
+ ## Gotchas
59
+
60
+ - Presigned URLs bypass Lambda (upload goes directly to S3)
61
+ - MIME validation happens both at presign time and on confirm
62
+ - Ownership is enforced: users can only list/delete their own files
63
+ - `auth.verifyToken` must be configured (default-deny without it)
@@ -0,0 +1,118 @@
1
+ # Testing Patterns
2
+
3
+ > Jest setup, TDD workflow, and testing conventions for everystack packages.
4
+
5
+ ## When to Use
6
+ Read this when writing tests for everystack apps or packages.
7
+
8
+ ## TDD Workflow
9
+
10
+ 1. Write a failing test that describes expected behavior
11
+ 2. Implement the minimum code to make it pass
12
+ 3. Refactor while keeping tests green
13
+ 4. No features without tests. No tests without features.
14
+
15
+ ## Test Structure
16
+
17
+ ```
18
+ __tests__/
19
+ ├── handler/
20
+ │ ├── get.test.ts # GET endpoint tests
21
+ │ ├── post.test.ts # POST endpoint tests
22
+ │ ├── filters.test.ts # Filter operator tests
23
+ │ └── relations.test.ts # Relation embedding tests
24
+ ├── client/
25
+ │ └── client.test.ts # Client SDK tests
26
+ ├── schema.test.ts # Schema validation tests
27
+ └── integration/
28
+ └── full-flow.test.ts # End-to-end tests
29
+ ```
30
+
31
+ Files mirror `src/` structure. Named `{feature}.test.ts`.
32
+
33
+ ## Jest Configuration
34
+
35
+ Every package extends the base config:
36
+ ```javascript
37
+ // jest.config.js
38
+ const { config } = require('../../jest.config.base');
39
+ module.exports = { ...config };
40
+ ```
41
+
42
+ For packages without database needs, override global setup:
43
+ ```javascript
44
+ module.exports = {
45
+ ...config,
46
+ globalSetup: undefined,
47
+ globalTeardown: undefined,
48
+ setupFiles: [],
49
+ };
50
+ ```
51
+
52
+ ## Test Patterns
53
+
54
+ ### Handler Tests
55
+ ```typescript
56
+ describe('GET /posts', () => {
57
+ it('returns all posts', async () => {
58
+ const handler = createHandler(db, schema);
59
+ const req = new Request('http://localhost/posts');
60
+ const res = await handler(req);
61
+ expect(res.status).toBe(200);
62
+ const data = await res.json();
63
+ expect(data).toHaveLength(3);
64
+ });
65
+
66
+ it('filters by status', async () => {
67
+ const req = new Request('http://localhost/posts?status=eq.published');
68
+ const res = await handler(req);
69
+ const data = await res.json();
70
+ expect(data.every((p: any) => p.status === 'published')).toBe(true);
71
+ });
72
+ });
73
+ ```
74
+
75
+ ### Database Integration Tests
76
+ The test framework creates a template database and clones it per test suite for isolation:
77
+ ```typescript
78
+ // Tests get a fresh database with schema applied
79
+ // No cleanup needed between tests (each suite gets its own DB)
80
+ ```
81
+
82
+ ### Mocking Patterns
83
+
84
+ **ESM module mocking (required for dynamic imports):**
85
+ ```typescript
86
+ jest.unstable_mockModule('@aws-sdk/client-s3', () => ({
87
+ S3Client: jest.fn(),
88
+ PutObjectCommand: jest.fn(),
89
+ }));
90
+ ```
91
+
92
+ **Sharp mock (for image processing):**
93
+ ```typescript
94
+ jest.unstable_mockModule('sharp', () => ({
95
+ __esModule: true,
96
+ default: jest.fn(() => ({
97
+ resize: jest.fn().mockReturnThis(),
98
+ toBuffer: jest.fn().mockResolvedValue(Buffer.from('mock')),
99
+ })),
100
+ }));
101
+ ```
102
+
103
+ ## Running Tests
104
+
105
+ ```bash
106
+ pnpm test # All packages (via turbo)
107
+ pnpm --filter @everystack/api test # Single package
108
+ npx jest __tests__/handler/get.test.ts # Single file (from package dir)
109
+ npx jest --watch # Watch mode
110
+ ```
111
+
112
+ ## Gotchas
113
+
114
+ - Tests require `--experimental-vm-modules` for ESM mocking
115
+ - `jest.unstable_mockModule` + top-level `await` needed for ESM modules
116
+ - Template database requires PostgreSQL 16 running locally or `DATABASE_URL` set
117
+ - `turbo test` has `cache: false` (tests always run, never cached)
118
+ - `sharp` mock needs `__esModule: true` for dynamic `import()` pattern
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@everystack/mcp",
3
- "version": "0.2.3",
4
- "description": "MCP server that makes Claude an everystack expert",
3
+ "version": "0.3.0",
4
+ "description": "Governance layer that governs how any agent builds everystack — grounding, cheat gates, and Model-aware tooling over MCP",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
7
7
  "repository": {
@@ -17,6 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "files": [
20
+ "dist",
20
21
  "src",
21
22
  "README.md"
22
23
  ],
@@ -27,23 +28,24 @@
27
28
  }
28
29
  },
29
30
  "bin": {
30
- "everystack-mcp": "./src/index.ts"
31
- },
32
- "dependencies": {
33
- "@modelcontextprotocol/sdk": "1.29.0",
34
- "zod": "3.25.67"
31
+ "everystack-mcp": "./dist/index.cjs"
35
32
  },
36
33
  "devDependencies": {
34
+ "@modelcontextprotocol/sdk": "1.29.0",
37
35
  "@types/jest": "29.5.14",
38
36
  "@types/node": "22.19.18",
37
+ "esbuild": "0.25.12",
39
38
  "jest": "29.7.0",
40
39
  "ts-jest": "29.4.9",
41
40
  "tsx": "4.21.0",
42
- "typescript": "5.9.3"
41
+ "typescript": "5.9.3",
42
+ "zod": "3.25.67",
43
+ "@everystack/cli": "0.3.6",
44
+ "@everystack/model": "0.3.3"
43
45
  },
44
46
  "scripts": {
45
47
  "test": "jest",
46
- "build": "tsc --build",
48
+ "build": "node scripts/build.mjs",
47
49
  "lint": "tsc --noEmit"
48
50
  }
49
51
  }