Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e35d908672 | ||
|
|
c0db8f6165 | ||
|
|
fd4e69a27c | ||
|
|
1ccb031dba |
@@ -12,3 +12,11 @@ screenshot-*.png
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.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, source: https://github.com/garrytan/gstack (agents-digest/gstack-AGENTS.md) -->
|
||||||
|
|
||||||
# gstack digest v1.91.12.0 — regenerate/re-copy after upgrading gstack
|
# gstack digest v1.91.12.0 — regenerate/re-copy after upgrading gstack
|
||||||
|
|
||||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
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
|
## The reuse ladder
|
||||||
|
|
||||||
Before writing new code, stop at the first rung that holds:
|
Before writing new code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. A helper, util, or pattern already in this repo.
|
1. A helper, util, or pattern already in this repo.
|
||||||
2. The standard library.
|
2. The standard library.
|
||||||
3. A native platform feature (CSS over JS, DB constraint over app code).
|
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.
|
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)
|
## 1. Stack (pinned 2026-10-11)
|
||||||
|
|
||||||
| Layer | Choice | Version |
|
| 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.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 |
|
| CMS / API / admin | Payload 3 (Next-native, Lexical rich text) | ^3.60.0 |
|
||||||
| Database | PostgreSQL | postgres:16 (container) |
|
| Database | PostgreSQL | postgres:16 (container) |
|
||||||
| DB adapter | @payloadcms/db-postgres (Drizzle migrations) | ^3.60.0 |
|
| DB adapter | @payloadcms/db-postgres (Drizzle migrations) | ^3.60.0 |
|
||||||
| UI runtime | React | ^19.2.0 |
|
| UI runtime | React | ^19.2.0 |
|
||||||
| Styling | Hand-rolled CSS (`src/app/design.css` + per-route CSS). No Tailwind, no component library. | — |
|
| 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()`) |
|
| Payments | stripe | ^18.5.0 (scaffolded, gated by `billingEnabled()`) |
|
||||||
| Email | Cloudflare Email REST API (ADR-0003) — **never SMTP** | — |
|
| Email | Cloudflare Email REST API (ADR-0003) — **never SMTP** | — |
|
||||||
| Language | TypeScript strict | ^5.9.0 |
|
| 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) | — |
|
| Deploy | Docker Compose on VPS `root@147.79.102.201:/root/anwarpi`, Traefik proxy network, apex anwarpy.ai (www 308) | — |
|
||||||
|
|
||||||
## 2. Repository layout
|
## 2. Repository layout
|
||||||
|
|
||||||
@@ -55,18 +55,18 @@ launch.ts (policy) + doc.access + doc.status → effective access
|
|||||||
|
|
||||||
## 4. Module boundaries (`src/lib/`)
|
## 4. Module boundaries (`src/lib/`)
|
||||||
|
|
||||||
| Module | Owns | Must not |
|
| Module | Owns | Must not |
|
||||||
|---|---|---|
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||||
| `launch.ts` | LAUNCH_MODE policy, WPM constant | read env elsewhere for launch state |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `agent-media.ts` / `agent-files.ts` | pipeline ingestion of media/attachments | accept `access` from the payload |
|
||||||
| `next.ts` | shared Next helpers | — |
|
| `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.
|
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 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 |
|
| 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 |
|
| `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 |
|
| `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]/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]/like` | session required | one per user per post |
|
||||||
| `POST /api/posts/[slug]/view` | public | anonymous view counter |
|
| `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/notify` | public | always records; honest 503 when mail transport down; dup → `alreadySubscribed` |
|
||||||
| `POST /api/subscribe` | public | 503 honest Arabic while billing disabled |
|
| `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 |
|
| `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 |
|
| `GET /api/files/[id]` | per-file access gate | streams attachments |
|
||||||
| `POST /api/stripe/webhook` | signature | no-op while `billingEnabled()` is false |
|
| `POST /api/stripe/webhook` | signature | no-op while `billingEnabled()` is false |
|
||||||
|
|
||||||
**Data access patterns:**
|
**Data access patterns:**
|
||||||
|
|
||||||
- Server components use the **Payload Local API** (`payload.find`, `payload.findByID`) — never internal fetch to own routes.
|
- 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.
|
- 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.
|
- 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
|
## Create + publish a post with an image
|
||||||
|
|
||||||
```bash
|
```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)
|
# Inline base64 image (jpeg/png/webp, ≤5MB)
|
||||||
python3 - <<'EOF' > /tmp/post.json
|
python3 - <<'EOF' > /tmp/post.json
|
||||||
@@ -54,11 +54,20 @@ Response `201`:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": 113, "title": "دليلي الجديد", "slug": "my-new-guide",
|
"id": 113,
|
||||||
"status": "published", "access": "vip", "category": "agents",
|
"title": "دليلي الجديد",
|
||||||
|
"slug": "my-new-guide",
|
||||||
|
"status": "published",
|
||||||
|
"access": "vip",
|
||||||
|
"category": "agents",
|
||||||
"cover": { "id": 113, "url": "/api/media/file/my-cover.jpg" },
|
"cover": { "id": 113, "url": "/api/media/file/my-cover.jpg" },
|
||||||
"attachments": [
|
"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"
|
"covers": "node scripts/generate-covers.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clerk/nextjs": "^7.9.14",
|
||||||
"@payloadcms/db-postgres": "^3.60.0",
|
"@payloadcms/db-postgres": "^3.60.0",
|
||||||
"@payloadcms/next": "^3.60.0",
|
"@payloadcms/next": "^3.60.0",
|
||||||
"@payloadcms/richtext-lexical": "^3.60.0",
|
"@payloadcms/richtext-lexical": "^3.60.0",
|
||||||
"next": "15.4.11",
|
"next": "^15.5.27",
|
||||||
"payload": "^3.60.0",
|
"payload": "^3.60.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"sharp": "^0.34.0",
|
"sharp": "^0.35.5",
|
||||||
"stripe": "^18.5.0"
|
"stripe": "^18.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.64.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/nodemailer": "^8.0.2",
|
"@types/nodemailer": "^8.0.2",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^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": {
|
"engines": {
|
||||||
"node": ">=20.9.0"
|
"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