Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
|
||||
|
||||
|
||||
+37
-36
@@ -4,18 +4,18 @@
|
||||
|
||||
## 1. Stack (pinned 2026-10-11)
|
||||
|
||||
| Layer | Choice | Version |
|
||||
|---|---|---|
|
||||
| Framework | Next.js App Router (server components first) | next@15.4.11 |
|
||||
| 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 |
|
||||
| UI runtime | React | ^19.2.0 |
|
||||
| Styling | Hand-rolled CSS (`src/app/design.css` + per-route CSS). No Tailwind, no component library. | — |
|
||||
| Payments | stripe | ^18.5.0 (scaffolded, gated by `billingEnabled()`) |
|
||||
| Email | Cloudflare Email REST API (ADR-0003) — **never SMTP** | — |
|
||||
| Language | TypeScript strict | ^5.9.0 |
|
||||
| Deploy | Docker Compose on VPS `root@147.79.102.201:/root/anwarpi`, Traefik proxy network, apex anwarpy.ai (www 308) | — |
|
||||
| Layer | Choice | Version |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| Framework | Next.js App Router (server components first) | next@15.5.27 (CVE-driven bump from 15.4.11, owner-approved 2026-10-13) |
|
||||
| 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 |
|
||||
| UI runtime | React | ^19.2.0 |
|
||||
| Styling | Hand-rolled CSS (`src/app/design.css` + per-route CSS). No Tailwind, no component library. | — |
|
||||
| Payments | stripe | ^18.5.0 (scaffolded, gated by `billingEnabled()`) |
|
||||
| Email | Cloudflare Email REST API (ADR-0003) — **never SMTP** | — |
|
||||
| Language | TypeScript strict | ^5.9.0 |
|
||||
| Deploy | Docker Compose on VPS `root@147.79.102.201:/root/anwarpi`, Traefik proxy network, apex anwarpy.ai (www 308) | — |
|
||||
|
||||
## 2. Repository layout
|
||||
|
||||
@@ -55,18 +55,18 @@ 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 |
|
||||
| `secret-scan.ts` | `scanForSecrets`/`scanAgentPayload` — pattern names only in responses | log or return matched secret text |
|
||||
| `llms.ts` | llms.txt/full rendering, `fetchIndexDocs(withContent)` | bypass the access decision |
|
||||
| `topics.ts` | TOPICS registry (agents/models/automation/foundations), `topicsFor()`, `isTopic()` | hardcode slugs elsewhere |
|
||||
| `format.ts` | Arabic number/label agreement (`guidesLabel`, `minutesLabel`, `MEMBERSHIP_PRICE`) | duplicate Arabic plurals inline |
|
||||
| `mail.ts` | Cloudflare Email REST send | use SMTP; log recipient addresses |
|
||||
| `agent-media.ts` / `agent-files.ts` | pipeline ingestion of media/attachments | accept `access` from the payload |
|
||||
| `next.ts` | shared Next helpers | — |
|
||||
| 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 |
|
||||
| `secret-scan.ts` | `scanForSecrets`/`scanAgentPayload` — pattern names only in responses | log or return matched secret text |
|
||||
| `llms.ts` | llms.txt/full rendering, `fetchIndexDocs(withContent)` | bypass the access decision |
|
||||
| `topics.ts` | TOPICS registry (agents/models/automation/foundations), `topicsFor()`, `isTopic()` | hardcode slugs elsewhere |
|
||||
| `format.ts` | Arabic number/label agreement (`guidesLabel`, `minutesLabel`, `MEMBERSHIP_PRICE`) | duplicate Arabic plurals inline |
|
||||
| `mail.ts` | Cloudflare Email REST send | use SMTP; log recipient addresses |
|
||||
| `agent-media.ts` / `agent-files.ts` | pipeline ingestion of media/attachments | accept `access` from the payload |
|
||||
| `next.ts` | shared Next helpers | — |
|
||||
|
||||
Import rule: pages/components may import lib + payload; lib modules never import each other's internals — if two libs need shared logic, promote it to the lower-level module.
|
||||
|
||||
@@ -74,20 +74,21 @@ 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) |
|
||||
| `POST /api/posts/[slug]/like` | session required | one per user per post |
|
||||
| `POST /api/posts/[slug]/view` | public | anonymous view counter |
|
||||
| `POST /api/notify` | public | always records; honest 503 when mail transport down; dup → `alreadySubscribed` |
|
||||
| `POST /api/subscribe` | public | 503 honest Arabic while billing disabled |
|
||||
| `POST /api/users`, `/api/auth/verify-email`, `/api/auth/resend-verification` | public / token | verification-token writes at signup; middleware rate-limits this path |
|
||||
| `GET /api/files/[id]` | per-file access gate | streams attachments |
|
||||
| `POST /api/stripe/webhook` | signature | no-op while `billingEnabled()` is false |
|
||||
| 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) |
|
||||
| `POST /api/posts/[slug]/like` | session required | one per user per post |
|
||||
| `POST /api/posts/[slug]/view` | public | anonymous view counter |
|
||||
| `POST /api/notify` | public | always records; honest 503 when mail transport down; dup → `alreadySubscribed` |
|
||||
| `POST /api/subscribe` | public | 503 honest Arabic while billing disabled |
|
||||
| `POST /api/users`, `/api/auth/verify-email`, `/api/auth/resend-verification` | public / token | verification-token writes at signup; middleware rate-limits this path |
|
||||
| `GET /api/files/[id]` | per-file access gate | streams attachments |
|
||||
| `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.
|
||||
|
||||
@@ -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
+3657
-904
File diff suppressed because it is too large
Load Diff
+15
-3
@@ -12,24 +12,36 @@
|
||||
"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",
|
||||
"next": "15.4.11",
|
||||
"next": "^15.5.27",
|
||||
"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