Compare commits
5
Commits
v1.1.0
...
36f508df75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f508df75 | ||
|
|
26d662ed7c | ||
|
|
e35d908672 | ||
|
|
c0db8f6165 | ||
|
|
fd4e69a27c |
@@ -12,3 +12,11 @@ screenshot-*.png
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Local AI coding hosts (procoder init); .procoder/ stays trackable.
|
||||
/skills/procoder/
|
||||
|
||||
# procoder state (regenerable)
|
||||
.procoder/state/
|
||||
.procoder/index/
|
||||
.lycheecache
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[git]
|
||||
default_branch_policy = "report" # rulebook allows docs/evidence commits to main
|
||||
@@ -0,0 +1,13 @@
|
||||
# Context — anwarpy.ai
|
||||
|
||||
## effective access
|
||||
|
||||
The single server-side decision (launch.ts policy + doc.access + doc.status) consumed identically by the article page, public JSON route, download gate, and llms-full.
|
||||
|
||||
## ghost-read
|
||||
|
||||
Fetching an intended-VIP post logged out to assert what a stranger sees; the acceptance check for any access-logic change.
|
||||
|
||||
## honest 503
|
||||
|
||||
A dependency-down response with a fixed Arabic sentence (e.g. billing disabled, mail transport down) instead of a fake success; reference implementation: /api/notify.
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"skills"
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": 1,
|
||||
"synced_at": "2026-10-13T02:31:48Z",
|
||||
"lockfiles": {
|
||||
"web/package-lock.json": "b0168d824c755b061aeb7dc8bddb7faa05b7a236ccc82b196843d5752e3632d3"
|
||||
},
|
||||
"migrations": {
|
||||
"web/src/migrations": {
|
||||
"count": 14,
|
||||
"entries": [
|
||||
"20261002_130505_init.json",
|
||||
"20261002_130505_init.ts",
|
||||
"20261002_170000_add_full_name.ts",
|
||||
"20261003_111451.json",
|
||||
"20261003_111451.ts",
|
||||
"20261003_133758.json",
|
||||
"20261003_133758.ts",
|
||||
"20261003_201454.json",
|
||||
"20261003_201454.ts",
|
||||
"20261009_130817.json",
|
||||
"20261009_130817.ts",
|
||||
"20261010_142214.json",
|
||||
"20261010_142214.ts",
|
||||
"index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"env_keys": {}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<!-- gstack digest, source: https://github.com/garrytan/gstack (agents-digest/gstack-AGENTS.md) -->
|
||||
|
||||
# gstack digest v1.91.12.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
@@ -15,6 +16,7 @@ reviews, and evals on top of these rules.
|
||||
## The reuse ladder
|
||||
|
||||
Before writing new code, stop at the first rung that holds:
|
||||
|
||||
1. A helper, util, or pattern already in this repo.
|
||||
2. The standard library.
|
||||
3. A native platform feature (CSS over JS, DB constraint over app code).
|
||||
@@ -86,4 +88,3 @@ ssh root@147.79.102.201 'cd /root/anwarpi && docker compose -f docker-compose.vp
|
||||
```
|
||||
|
||||
Prod scripts (no `scripts/` in the standalone image): `ssh -f -N -L 15432:<db-container-ip>:5432 root@…`, then locally `set -a; source <tmp env>; set +a; DATABASE_URI=postgresql://anwarpi:$DB_PASSWORD@127.0.0.1:15432/anwarpi npx tsx scripts/<script>.ts`; delete the temp env file after. Never echo the password.
|
||||
|
||||
|
||||
+5
-4
@@ -5,8 +5,8 @@
|
||||
## 1. Stack (pinned 2026-10-11)
|
||||
|
||||
| Layer | Choice | Version |
|
||||
|---|---|---|
|
||||
| Framework | Next.js App Router (server components first) | next@15.4.11 |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| Framework | Next.js App Router (server components first) | next@15.4.11 — ceiling of `@payloadcms/next` peer support; known CVEs risk-accepted (SECURITY.md §8) |
|
||||
| CMS / API / admin | Payload 3 (Next-native, Lexical rich text) | ^3.60.0 |
|
||||
| Database | PostgreSQL | postgres:16 (container) |
|
||||
| DB adapter | @payloadcms/db-postgres (Drizzle migrations) | ^3.60.0 |
|
||||
@@ -56,7 +56,7 @@ launch.ts (policy) + doc.access + doc.status → effective access
|
||||
## 4. Module boundaries (`src/lib/`)
|
||||
|
||||
| Module | Owns | Must not |
|
||||
|---|---|---|
|
||||
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
| `launch.ts` | LAUNCH_MODE policy, WPM constant | read env elsewhere for launch state |
|
||||
| `viewer.ts` | `getViewer()` — session → `{email, role, id, interest?, level?, emailVerified?, onboarded?}` | touch collections directly beyond auth |
|
||||
| `lexical.ts` | Lexical serialization: `lexicalToText`, `countLexicalWords` (walks `content.root`!), `extractH2s`, `lexicalToPlain` | know about Posts/DB |
|
||||
@@ -75,7 +75,7 @@ Import rule: pages/components may import lib + payload; lib modules never import
|
||||
**Route handlers** (`src/app/api/**/route.ts`) return `NextResponse.json`. Every error body is `{"error": "<real Arabic sentence>"}` — honest, specific, never a bare code.
|
||||
|
||||
| Route | Auth | Notes |
|
||||
|---|---|---|
|
||||
| ---------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `POST /api/agent/posts`, `PUT /api/agent/posts/[slug]` | `x-agent-key` header | pipeline; 401 bad key, 400 shape, 409 slug conflict (after sourceId dedup), 422 secret scan |
|
||||
| `GET /api/posts/[slug]` | public | JSON honoring the access decision |
|
||||
| `POST /api/posts/[slug]/comments` | session required | server-forced author; moderated status (ADR-0001/0002) |
|
||||
@@ -88,6 +88,7 @@ Import rule: pages/components may import lib + payload; lib modules never import
|
||||
| `POST /api/stripe/webhook` | signature | no-op while `billingEnabled()` is false |
|
||||
|
||||
**Data access patterns:**
|
||||
|
||||
- Server components use the **Payload Local API** (`payload.find`, `payload.findByID`) — never internal fetch to own routes.
|
||||
- Client components are thin: forms POST to route handlers and render the returned Arabic message. No client data-fetching library, no global store; if a page needs reactive data, it re-fetches via a route handler or uses Payload's REST with credentials.
|
||||
- Generated types: `npm run generate:types` (runs as part of `build`) — collections' `Collection` types come from `payload-types.ts`, never hand-written interfaces for docs.
|
||||
|
||||
+12
-2
@@ -13,7 +13,7 @@
|
||||
## 2. Environment variables & secrets
|
||||
|
||||
| Var | Scope | Rule |
|
||||
|---|---|---|
|
||||
| --------------------------------------------------------------- | -------- | --------------------------------------------------- |
|
||||
| `DATABASE_URI` / `DB_PASSWORD` | server | compose builds the URI; never commit, never echo |
|
||||
| `PAYLOAD_SECRET` | server | signing/encryption; rotate = invalidate sessions |
|
||||
| `AGENT_API_KEY` | server | pipeline auth; header `x-agent-key` |
|
||||
@@ -24,6 +24,7 @@
|
||||
| `SEED_TEST_USERS` | dev only | never set in prod |
|
||||
|
||||
Rules:
|
||||
|
||||
- `.env*` files are gitignored and **excluded from rsync deploys**; prod secrets live only in `/root/anwarpi/.env.production`.
|
||||
- Never print secret values into transcripts, docs, videos, or evidence. Known incident: 8 chars of prod `DB_PASSWORD` leaked in a session transcript on 2026-10-10 → **owner to rotate**; rotation is the standing remedy for any partial leak.
|
||||
- Read secrets at runtime via `process.env` inside server modules only; never in `'use client'` code (only `NEXT_PUBLIC_*` there).
|
||||
@@ -38,7 +39,7 @@ Rules:
|
||||
## 4. Input handling & pipeline hardening
|
||||
|
||||
- **`pickFields`** on the agent API strips privileged fields (`access`, `readingMinutes`, `status` except explicit `publish: true` opt-in on POST; PUT cannot set `status`).
|
||||
- **`scanForSecrets` / `scanAgentPayload`** (`src/lib/secret-scan.ts`): POST and PUT scan the *real text* (Lexical → `lexicalToText`, or the raw string) for AWS AKIA keys, PEM blocks, `sk-` API keys, GitHub tokens (`ghp_`…), generic credential shapes → **422 with pattern NAMES only**. Never echo the matched text, never log it.
|
||||
- **`scanForSecrets` / `scanAgentPayload`** (`src/lib/secret-scan.ts`): POST and PUT scan the _real text_ (Lexical → `lexicalToText`, or the raw string) for AWS AKIA keys, PEM blocks, `sk-` API keys, GitHub tokens (`ghp_`…), generic credential shapes → **422 with pattern NAMES only**. Never echo the matched text, never log it.
|
||||
- String `content` → 400 (Lexical JSON required); no silent placeholder substitution.
|
||||
- Comment bodies: textarea, moderated (pending until approved), author forced server-side from the session — client-supplied `author`/`authorName` identity is ignored/overwritten (ADR-0002). Display names are a snapshot, not a live join, so renamed users can't retroactively forge old comments.
|
||||
- All user-facing error strings are fixed Arabic sentences from the route handlers — errors never reflect raw input back (no echo = reflected-input hygiene).
|
||||
@@ -62,3 +63,12 @@ Rules:
|
||||
- No WAF/Cloudflare proxy changes without owner approval — the domain's DNS/proxy is Ahmed's console.
|
||||
- No secrets in client bundles; no `NEXT_PUBLIC_` for anything except the site URL.
|
||||
- No new auth provider without routing it through `viewer.ts` (see §1).
|
||||
|
||||
## 8. Accepted risks
|
||||
|
||||
### next@15.4.11 — 28 known CVEs, max 9.5 (risk-accepted by owner, 2026-10-13)
|
||||
|
||||
- **Why pinned**: `@payloadcms/next@3.90.2` peer-supports next `<15.5.0 || >=16.3.3` — no Payload 3.x release supports 15.5.x, so the safe ladder is 15.4.11 → (Payload 4 or a verified Next 16 migration). Forcing 15.5.x would run the CMS on an unsupported combo.
|
||||
- **Exposure analysis**: the 9.5-critical (Image Optimizer DoS, GHSA-9g9p-9gw9-jx7f) requires attacker-reachable `remotePatterns` — this app configures none (local covers only). The cache-poisoning advisory targets SSG/ISR caching; the site is predominantly dynamic SSR via the Payload Local API.
|
||||
- **Revisit trigger**: any of — Payload publishes 15.5/16 peer support (then upgrade next in one verified PR), Payload 4 adoption, or a new next advisory with remote-code-execution class impact.
|
||||
- **Gate note**: `procoder check` re-flags this on any `package-lock.json` change. If you must touch the lockfile before upgrading, either upgrade next in the same PR or skip the gate once with the reason in the commit message. Transitive CVE fixes already applied: undici 8.11.2, sharp 0.35.5, postcss 8.5.30, sass 1.105.1.
|
||||
|
||||
@@ -24,7 +24,7 @@ Publish and manage blog posts programmatically. Auth: send the agent key in the
|
||||
## Create + publish a post with an image
|
||||
|
||||
```bash
|
||||
KEY=033d1eed44ca6abf483de10735c47155d38f7b5bc9d46449
|
||||
KEY="$AGENT_API_KEY" # never hardcode — export AGENT_API_KEY in your shell; it lives in the server's .env.production
|
||||
|
||||
# Inline base64 image (jpeg/png/webp, ≤5MB)
|
||||
python3 - <<'EOF' > /tmp/post.json
|
||||
@@ -54,11 +54,20 @@ Response `201`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 113, "title": "دليلي الجديد", "slug": "my-new-guide",
|
||||
"status": "published", "access": "vip", "category": "agents",
|
||||
"id": 113,
|
||||
"title": "دليلي الجديد",
|
||||
"slug": "my-new-guide",
|
||||
"status": "published",
|
||||
"access": "vip",
|
||||
"category": "agents",
|
||||
"cover": { "id": 113, "url": "/api/media/file/my-cover.jpg" },
|
||||
"attachments": [
|
||||
{ "id": 42, "filename": "agent-state.md", "access": "vip", "url": "/api/files/42" }
|
||||
{
|
||||
"id": 42,
|
||||
"filename": "agent-state.md",
|
||||
"access": "vip",
|
||||
"url": "/api/files/42"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Minimal flat config — procoder's lint domain checks .ts/.tsx with this.
|
||||
// Report-only by default (see .procoder/config.toml policy); add rules as needed.
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
".next/**",
|
||||
"node_modules/**",
|
||||
"src/payload-types.ts", // generated — do not lint
|
||||
"src/migrations/**",
|
||||
"media/**",
|
||||
"files-uploads/**",
|
||||
],
|
||||
},
|
||||
...tseslint.configs.recommended.map((c) => ({
|
||||
...c,
|
||||
files: ["**/*.{ts,tsx,mjs,cjs}"],
|
||||
})),
|
||||
{
|
||||
rules: {
|
||||
// Generated Payload types use `any` in payload-types.ts; local code may pass
|
||||
// narrow unknowns. Keep the floor permissive until a lint pass is scheduled.
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
Generated
+3395
-870
File diff suppressed because it is too large
Load Diff
+14
-2
@@ -12,6 +12,7 @@
|
||||
"covers": "node scripts/generate-covers.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^7.9.14",
|
||||
"@payloadcms/db-postgres": "^3.60.0",
|
||||
"@payloadcms/next": "^3.60.0",
|
||||
"@payloadcms/richtext-lexical": "^3.60.0",
|
||||
@@ -19,17 +20,28 @@
|
||||
"payload": "^3.60.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"sharp": "^0.34.0",
|
||||
"sharp": "^0.35.5",
|
||||
"stripe": "^18.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.64.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/nodemailer": "^8.0.2",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"typescript": "^5.9.0"
|
||||
"eslint": "^10.12.0",
|
||||
"prettier": "^3.9.10",
|
||||
"typescript": "^5.9.0",
|
||||
"typescript-eslint": "^8.71.2",
|
||||
"vitest": "^5.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"overrides": {
|
||||
"undici": "^8.11.2",
|
||||
"sharp": "^0.35.5",
|
||||
"postcss": "^8.5.6",
|
||||
"sass": "^1.79.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user