@everystack/mcp 0.2.3 → 0.3.1
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/README.md +37 -10
- package/dist/adding-database.md +169 -0
- package/dist/admin.md +81 -0
- package/dist/auth.md +115 -0
- package/dist/aws-setup.md +276 -0
- package/dist/cli.md +108 -0
- package/dist/client-api.md +145 -0
- package/dist/core.md +196 -0
- package/dist/deployment.md +146 -0
- package/dist/events.md +87 -0
- package/dist/first-run.md +100 -0
- package/dist/getting-started.md +75 -0
- package/dist/handler-options.md +114 -0
- package/dist/images.md +93 -0
- package/dist/index.cjs +23796 -0
- package/dist/jobs.md +97 -0
- package/dist/logging.md +91 -0
- package/dist/plugins.md +68 -0
- package/dist/project-claude-md.md +103 -0
- package/dist/query-protocol.md +129 -0
- package/dist/schema-patterns.md +167 -0
- package/dist/security-device.md +99 -0
- package/dist/security.md +270 -0
- package/dist/ssr.md +82 -0
- package/dist/storage.md +63 -0
- package/dist/testing.md +118 -0
- package/package.json +11 -9
- package/src/gates/detectors/embedded-data-bundle.ts +58 -0
- package/src/gates/detectors/hand-written-migration.ts +42 -0
- package/src/gates/detectors/secret-in-public-env.ts +41 -0
- package/src/gates/engine.ts +80 -0
- package/src/gates/registry.ts +25 -0
- package/src/gates/telemetry.ts +143 -0
- package/src/gates/types.ts +70 -0
- package/src/governance/cli.ts +193 -0
- package/src/governance/grounding.ts +344 -0
- package/src/index.ts +97 -50
- package/src/prompts/claude-md.ts +92 -0
- package/src/prompts/governance-setup.ts +85 -0
- package/src/prompts/index.ts +6 -0
- package/src/prompts/new-app.ts +4 -1
- package/src/prompts/runbook.ts +77 -0
- package/src/resources/project-claude-md.md +70 -94
- 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
|
package/dist/security.md
ADDED
|
@@ -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)
|
package/dist/storage.md
ADDED
|
@@ -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)
|
package/dist/testing.md
ADDED
|
@@ -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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
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": "./
|
|
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.12",
|
|
44
|
+
"@everystack/model": "0.3.4"
|
|
43
45
|
},
|
|
44
46
|
"scripts": {
|
|
45
47
|
"test": "jest",
|
|
46
|
-
"build": "
|
|
48
|
+
"build": "node scripts/build.mjs",
|
|
47
49
|
"lint": "tsc --noEmit"
|
|
48
50
|
}
|
|
49
51
|
}
|