commit 90e4dd0807d9745ab45efcb320a9cf111fa2f2fc Author: Bulma Date: Sat Mar 28 03:18:25 2026 +0000 feat(architecture): add complete technical architecture for SimpleNote - ARCHITECTURE.md: main architecture document - api-spec.yaml: full OpenAPI 3.0 spec - folder-structure.md: detailed folder layout - data-format.md: JSON schemas for .meta.json, .library.json, .tag-index.json - env-template.md: environment variables documentation - cli-protocol.md: CLI-to-API communication protocol diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1d7ed87 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,413 @@ +# SimpleNote - Arquitectura Técnica + +## 1. Visión General + +Sistema de gestión de documentos basado en archivos Markdown con API REST. Diseñado para reemplazar Joplin con una arquitectura más simple. + +``` +┌─────────────────┐ ┌──────────────────┐ +│ simplenote-cli │◄────►│ simplenote-web │ +│ (Commander.js) │ │ (Express + API) │ +└─────────────────┘ └────────┬─────────┘ + │ + ┌────────▼─────────┐ + │ File System │ + │ (Markdown + JSON)│ + └───────────────────┘ +``` + +## 2. Componentes + +### 2.1 simplenote-web +- **Rol**: API REST + Frontend web +- **Puerto default**: 3000 +- **Estructura interna**: Express.js con routers modulares + +### 2.2 simplenote-cli +- **Rol**: Cliente de línea de comandos +- **Conexión**: HTTP al API de simplenote-web +- **Config**: `~/.config/simplenote/config.json` + +## 3. Arquitectura de Datos + +### 3.1 Estructura de Librerías (Filesystem) + +``` +data/ # DATA_ROOT configurable +└── libraries/ + └── {library-id}/ + ├── .library.json # Metadata de librería + ├── documents/ + │ └── {document-id}/ + │ ├── index.md # Contenido + │ └── .meta.json # Metadata del documento + └── sub-libraries/ + └── {child-lib-id}/... # Anidamiento recursivo +``` + +### 3.2 JSON Manifests + +**`.library.json`** — Metadata de librería: +```json +{ + "id": "uuid-v4", + "name": "Nombre de Librería", + "parentId": "parent-uuid | null", + "path": "/libraries/uuid", + "createdAt": "ISO8601", + "updatedAt": "ISO8601" +} +``` + +**`.meta.json`** — Metadata de documento: +```json +{ + "id": "uuid-v4", + "title": "string", + "tags": ["tag1", "tag2"], + "type": "requirement|note|spec|general", + "status": "draft|approved|implemented", + "priority": "high|medium|low", + "createdBy": "agent-id", + "createdAt": "ISO8601", + "updatedAt": "ISO8601", + "libraryId": "uuid" +} +``` + +**`.tag-index.json`** — Índice global de tags (en DATA_ROOT): +```json +{ + "version": 1, + "updatedAt": "ISO8601", + "tags": { + "backend": ["doc-1", "doc-2"], + "api": ["doc-1", "doc-3"], + "auth": ["doc-2"] + } +} +``` + +### 3.3 Formato de Documento (Markdown) + +```markdown +--- +id: REQ-001 +title: Título del Requerimiento +type: requirement +priority: high +status: draft +tags: [backend, api] +createdBy: agent-id +createdAt: 2026-03-28 +--- + +# Título + +## Descripción +Descripción del requerimiento. + +## Criterios de Aceptación +- [ ] Criterio 1 +- [ ] Criterio 2 +``` + +## 4. API REST + +### 4.1 Base URL +``` +/api/v1 +``` + +### 4.2 Autenticación +- **Método**: Token Bearer en header `Authorization` +- **Formato**: `Authorization: Bearer ` +- **Admin tokens**: Generados en setup inicial, almacenados en `.auth-tokens.json` + +### 4.3 Endpoints + +#### Auth +| Method | Endpoint | Descripción | +|--------|----------|-------------| +| POST | `/api/v1/auth/token` | Generar token (admin) | +| GET | `/api/v1/auth/verify` | Verificar token válido | + +#### Documents +| Method | Endpoint | Descripción | +|--------|----------|-------------| +| GET | `/api/v1/documents` | Listar (filtros: tag, library, type) | +| GET | `/api/v1/documents/:id` | Obtener documento + metadata | +| POST | `/api/v1/documents` | Crear documento | +| PUT | `/api/v1/documents/:id` | Actualizar documento | +| DELETE | `/api/v1/documents/:id` | Eliminar documento | +| GET | `/api/v1/documents/:id/export` | Exportar como Markdown | + +#### Libraries +| Method | Endpoint | Descripción | +|--------|----------|-------------| +| GET | `/api/v1/libraries` | Listar librerías raíz | +| GET | `/api/v1/libraries/:id` | Ver contenido de librería | +| POST | `/api/v1/libraries` | Crear librería | +| GET | `/api/v1/libraries/:id/tree` | Árbol completo de sublibrerías | +| DELETE | `/api/v1/libraries/:id` | Eliminar librería | + +#### Tags +| Method | Endpoint | Descripción | +|--------|----------|-------------| +| GET | `/api/v1/tags` | Listar todos los tags | +| GET | `/api/v1/tags/:tag/documents` | Docs con tag específico | +| POST | `/api/v1/documents/:id/tags` | Agregar tags a documento | + +## 5. Middleware de Auth + +### 5.1 Flujo de Tokens + +1. **Setup inicial**: Se genera admin token (almacenado en `.auth-tokens.json`) +2. **CLI login**: `simplenote auth login ` → almacena token en config local +3. **Requests**: Token enviado en header `Authorization: Bearer ` +4. **Verificación**: Middleware busca token en `.auth-tokens.json` + +### 5.2 Implementación + +```javascript +// authMiddleware.js +async function authMiddleware(req, res, next) { + const token = req.headers.authorization?.replace('Bearer ', ''); + if (!token) { + return res.status(401).json({ error: 'Token required' }); + } + + const tokens = await readJSON('.auth-tokens.json'); + if (!tokens.valid.includes(token)) { + return res.status(401).json({ error: 'Invalid token' }); + } + + req.token = token; + next(); +} +``` + +### 5.3 Archivo `.auth-tokens.json` + +```json +{ + "version": 1, + "tokens": [ + { + "token": "snk_xxxxx", + "label": "cli-default", + "createdAt": "ISO8601" + } + ] +} +``` + +## 6. Estrategia de Indexación de Tags + +### 6.1write-through +Cuando se crea/actualiza/elimina un documento: +1. Se actualiza su `.meta.json` con los tags +2. Se reconstruye el `.tag-index.json` global + +### 6.2 Optimización +- El índice se rebuild en memoria al iniciar (rápido para <10k docs) +- Para sistemas grandes: rebuild incremental (solo afecta docs modificados) +- El índice es un archivo JSON plano para búsqueda O(1) por tag + +### 6.3 API de Búsqueda +```javascript +// GET /api/v1/tags/backend +// → Lee .tag-index.json → returns ["doc-1", "doc-2"] + +// GET /api/v1/documents?tag=backend +// → Busca en índice → filtra docs en memoria → retorna +``` + +## 7. CLI API Client + +### 7.1 Configuración Local +```json +// ~/.config/simplenote/config.json +{ + "apiUrl": "http://localhost:3000/api/v1", + "token": "snk_xxxxx", + "activeLibrary": "default" +} +``` + +### 7.2 Cliente HTTP + +```javascript +// cli/src/api/client.js +class SimpleNoteClient { + constructor(baseUrl, token) { + this.baseUrl = baseUrl; + this.token = token; + } + + async request(method, path, body) { + const res = await fetch(`${this.baseUrl}${path}`, { + method, + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : undefined + }); + + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || `HTTP ${res.status}`); + } + + return res.json(); + } + + // Documents + listDocuments(params) { return this.request('GET', '/documents', null); } + getDocument(id) { return this.request('GET', `/documents/${id}`); } + createDocument(data) { return this.request('POST', '/documents', data); } + updateDocument(id, data) { return this.request('PUT', `/documents/${id}`, data); } + deleteDocument(id) { return this.request('DELETE', `/documents/${id}`); } + + // Libraries + listLibraries() { return this.request('GET', '/libraries'); } + createLibrary(data) { return this.request('POST', '/libraries', data); } + + // Tags + listTags() { return this.request('GET', '/tags'); } + getTagDocuments(tag) { return this.request('GET', `/tags/${tag}/documents`); } + + // Auth + verifyToken() { return this.request('GET', '/auth/verify'); } +} +``` + +## 8. Dependencias NPM + +### simplenote-web +```json +{ + "dependencies": { + "express": "^4.18.2", + "uuid": "^9.0.0", + "marked": "^11.0.0", + "gray-matter": "^4.0.3" + }, + "devDependencies": { + "nodemon": "^3.0.0" + } +} +``` + +### simplenote-cli +```json +{ + "dependencies": { + "commander": "^11.1.0", + "axios": "^1.6.0" + } +} +``` + +## 9. Variables de Entorno + +| Variable | Default | Descripción | +|----------|---------|-------------| +| `PORT` | 3000 | Puerto del servidor | +| `DATA_ROOT` | `./data` | Raíz de documentos | +| `HOST` | `0.0.0.0` | Host de binding | +| `LOG_LEVEL` | `info` | Nivel de logging | +| `CORS_ORIGIN` | `*` | Orígenes CORS permitidos | + +## 10. Estructura de Archivos del Proyecto + +``` +simplenote-web/ +├── src/ +│ ├── index.js # Entry point +│ ├── app.js # Express setup +│ ├── routes/ +│ │ ├── documents.js +│ │ ├── libraries.js +│ │ ├── tags.js +│ │ └── auth.js +│ ├── services/ +│ │ ├── documentService.js +│ │ ├── libraryService.js +│ │ └── tagService.js +│ ├── middleware/ +│ │ └── auth.js +│ ├── utils/ +│ │ ├── markdown.js +│ │ ├── fsHelper.js +│ │ └── uuid.js +│ └── indexers/ +│ └── tagIndexer.js +├── data/ # .gitkeep +├── tests/ +├── package.json +└── README.md + +simplenote-cli/ +├── src/ +│ ├── index.js # Entry point (Commander) +│ ├── commands/ +│ │ ├── doc.js +│ │ ├── lib.js +│ │ └── tag.js +│ ├── api/ +│ │ └── client.js +│ └── config/ +│ └── loader.js +├── package.json +└── README.md +``` + +## 11. Flujo de Operaciones + +### 11.1 Crear Documento (CLI) +``` +simplenote doc create --title "REQ-001" --tags "backend,api" + → POST /api/v1/documents + → Service: genera UUID, crea /data/libraries/{lib}/documents/{id}/index.md + → Service: crea /data/libraries/{lib}/documents/{id}/.meta.json + → Service: rebuild .tag-index.json + → Response: { id, path, tags, ... } +``` + +### 11.2 Crear Documento (Web) +``` +Form POST → /api/v1/documents + → Mismo flujo que CLI + → Web reconstruye lista con filtros +``` + +### 11.3 Búsqueda por Tag +``` +GET /api/v1/tags/backend + → tagIndexer: lee .tag-index.json + → Returns: ["doc-id-1", "doc-id-2"] + +GET /api/v1/documents?tag=backend + → Busca en índice → filtra docs → retorna con metadata +``` + +## 12. Seguridad + +- Tokens con prefijo `snk_` para identificación fácil +- Tokens almacenados en texto plano (sistema multi-usuario confiado) +- Para producción futura: hash bcrypt + salt +- CORS configurable para restringir acceso web + +## 13. Líneas de Código Estimadas + +| Componente | LOC aprox | +|------------|-----------| +| API REST (Express) | ~600 | +| Services (document, library, tag) | ~400 | +| Middleware auth | ~50 | +| Tag indexer | ~100 | +| CLI client | ~300 | +| CLI commands | ~400 | +| **Total** | ~1850 | diff --git a/api-spec.yaml b/api-spec.yaml new file mode 100644 index 0000000..3de65e0 --- /dev/null +++ b/api-spec.yaml @@ -0,0 +1,877 @@ +openapi: 3.0.3 +info: + title: SimpleNote API + description: REST API for SimpleNote document management system + version: 1.0.0 + contact: + name: SimpleNote Team + +servers: + - url: http://localhost:3000/api/v1 + description: Local development server + +tags: + - name: Auth + description: Authentication and token management + - name: Documents + description: Document CRUD operations + - name: Libraries + description: Library (folder) management + - name: Tags + description: Tag-based search and management + +paths: + # ============ AUTH ============ + /auth/token: + post: + tags: [Auth] + summary: Generate a new API token + description: Admin-only endpoint to generate a new bearer token + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [label] + properties: + label: + type: string + description: Human-readable label for the token + example: "cli-default" + responses: + '200': + description: Token generated successfully + content: + application/json: + schema: + type: object + properties: + token: + type: string + example: "snk_a1b2c3d4e5f6..." + label: + type: string + example: "cli-default" + createdAt: + type: string + format: date-time + '401': + description: Unauthorized - invalid admin token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '400': + description: Missing required fields + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /auth/verify: + get: + tags: [Auth] + summary: Verify current token is valid + security: + - BearerAuth: [] + responses: + '200': + description: Token is valid + content: + application/json: + schema: + type: object + properties: + valid: + type: boolean + example: true + token: + type: string + example: "snk_a1b2c3d4e5f6..." + '401': + description: Token is invalid or missing + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # ============ DOCUMENTS ============ + /documents: + get: + tags: [Documents] + summary: List all documents + security: + - BearerAuth: [] + parameters: + - name: tag + in: query + description: Filter by tag + schema: + type: string + example: "backend" + - name: library + in: query + description: Filter by library ID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440000" + - name: type + in: query + description: Filter by document type + schema: + type: string + enum: [requirement, note, spec, general] + example: "requirement" + - name: status + in: query + description: Filter by status + schema: + type: string + enum: [draft, approved, implemented] + example: "draft" + - name: limit + in: query + description: Max results to return + schema: + type: integer + default: 50 + example: 20 + - name: offset + in: query + description: Skip first N results + schema: + type: integer + default: 0 + example: 0 + responses: + '200': + description: List of documents + content: + application/json: + schema: + type: object + properties: + documents: + type: array + items: + $ref: '#/components/schemas/Document' + total: + type: integer + example: 42 + limit: + type: integer + example: 20 + offset: + type: integer + example: 0 + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + post: + tags: [Documents] + summary: Create a new document + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [title, libraryId] + properties: + title: + type: string + description: Document title + example: "API Authentication Design" + libraryId: + type: string + description: Target library ID + example: "550e8400-e29b-41d4-a716-446655440000" + content: + type: string + description: Markdown content (optional, defaults to template) + example: "# API Authentication\n\n## Description\n..." + tags: + type: array + items: + type: string + example: ["backend", "api", "auth"] + type: + type: string + enum: [requirement, note, spec, general] + default: general + priority: + type: string + enum: [high, medium, low] + default: medium + status: + type: string + enum: [draft, approved, implemented] + default: draft + responses: + '201': + description: Document created + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Library not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /documents/{id}: + get: + tags: [Documents] + summary: Get a document by ID + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Document UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440001" + responses: + '200': + description: Document found + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + tags: [Documents] + summary: Update a document + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Document UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440001" + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + title: + type: string + example: "Updated Title" + content: + type: string + example: "# Updated Content\n\nNew markdown..." + tags: + type: array + items: + type: string + example: ["backend", "api"] + type: + type: string + enum: [requirement, note, spec, general] + priority: + type: string + enum: [high, medium, low] + status: + type: string + enum: [draft, approved, implemented] + responses: + '200': + description: Document updated + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + delete: + tags: [Documents] + summary: Delete a document + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Document UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440001" + responses: + '200': + description: Document deleted + content: + application/json: + schema: + type: object + properties: + deleted: + type: boolean + example: true + id: + type: string + example: "550e8400-e29b-41d4-a716-446655440001" + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /documents/{id}/export: + get: + tags: [Documents] + summary: Export document as raw Markdown + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Document UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440001" + responses: + '200': + description: Raw Markdown file + content: + text/markdown: + schema: + type: string + example: | + --- + id: REQ-001 + title: API Authentication + --- + # API Authentication + + ## Description + ... + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /documents/{id}/tags: + post: + tags: [Tags] + summary: Add tags to a document + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Document UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440001" + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [tags] + properties: + tags: + type: array + items: + type: string + example: ["new-tag", "another-tag"] + responses: + '200': + description: Tags added + content: + application/json: + schema: + $ref: '#/components/schemas/Document' + '400': + description: Invalid tags array + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Document not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # ============ LIBRARIES ============ + /libraries: + get: + tags: [Libraries] + summary: List root-level libraries + security: + - BearerAuth: [] + responses: + '200': + description: List of root libraries + content: + application/json: + schema: + type: object + properties: + libraries: + type: array + items: + $ref: '#/components/schemas/Library' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + post: + tags: [Libraries] + summary: Create a new library + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [name] + properties: + name: + type: string + description: Library name + example: "Backend Requirements" + parentId: + type: string + nullable: true + description: Parent library ID for nesting (null for root) + example: "550e8400-e29b-41d4-a716-446655440000" + responses: + '201': + description: Library created + content: + application/json: + schema: + $ref: '#/components/schemas/Library' + '400': + description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Parent library not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /libraries/{id}: + get: + tags: [Libraries] + summary: Get library contents (documents and sub-libraries) + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Library UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440000" + responses: + '200': + description: Library contents + content: + application/json: + schema: + type: object + properties: + library: + $ref: '#/components/schemas/Library' + documents: + type: array + items: + $ref: '#/components/schemas/Document' + subLibraries: + type: array + items: + $ref: '#/components/schemas/Library' + '404': + description: Library not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /libraries/{id}/tree: + get: + tags: [Libraries] + summary: Get full subtree of a library + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Library UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440000" + responses: + '200': + description: Full library tree + content: + application/json: + schema: + $ref: '#/components/schemas/LibraryTree' + '404': + description: Library not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /libraries/{id}/documents: + get: + tags: [Documents] + summary: List documents in a specific library + security: + - BearerAuth: [] + parameters: + - name: id + in: path + required: true + description: Library UUID + schema: + type: string + example: "550e8400-e29b-41d4-a716-446655440000" + responses: + '200': + description: List of documents in library + content: + application/json: + schema: + type: object + properties: + libraryId: + type: string + documents: + type: array + items: + $ref: '#/components/schemas/Document' + '404': + description: Library not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + # ============ TAGS ============ + /tags: + get: + tags: [Tags] + summary: List all tags with document counts + security: + - BearerAuth: [] + responses: + '200': + description: All tags with counts + content: + application/json: + schema: + type: object + properties: + tags: + type: array + items: + type: object + properties: + name: + type: string + example: "backend" + count: + type: integer + example: 5 + total: + type: integer + example: 15 + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /tags/{tag}: + get: + tags: [Tags] + summary: Get all documents with a specific tag + security: + - BearerAuth: [] + parameters: + - name: tag + in: path + required: true + description: Tag name + schema: + type: string + example: "backend" + responses: + '200': + description: Documents with tag + content: + application/json: + schema: + type: object + properties: + tag: + type: string + example: "backend" + documents: + type: array + items: + $ref: '#/components/schemas/Document' + count: + type: integer + example: 5 + '404': + description: Tag not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: API token with `snk_` prefix + + schemas: + Error: + type: object + properties: + error: + type: string + example: "Document not found" + code: + type: string + example: "NOT_FOUND" + + Document: + type: object + properties: + id: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440001" + title: + type: string + example: "API Authentication Design" + path: + type: string + example: "/libraries/550e8400/.../documents/550e8401/index.md" + content: + type: string + description: Raw markdown content + example: "# API Authentication\n\n## Description\n..." + tags: + type: array + items: + type: string + example: ["backend", "api", "auth"] + type: + type: string + enum: [requirement, note, spec, general] + example: "requirement" + status: + type: string + enum: [draft, approved, implemented] + example: "draft" + priority: + type: string + enum: [high, medium, low] + example: "high" + libraryId: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + createdBy: + type: string + description: Agent or user ID who created the document + example: "agent-001" + createdAt: + type: string + format: date-time + example: "2026-03-28T10:00:00Z" + updatedAt: + type: string + format: date-time + example: "2026-03-28T12:30:00Z" + + Library: + type: object + properties: + id: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + name: + type: string + example: "Backend Requirements" + parentId: + type: string + nullable: true + format: uuid + description: Parent library ID, null for root + example: null + path: + type: string + description: Absolute path to library folder + example: "/data/libraries/550e8400" + documentCount: + type: integer + description: Total documents in this library (excludes sub-libraries) + example: 12 + createdAt: + type: string + format: date-time + example: "2026-03-28T09:00:00Z" + updatedAt: + type: string + format: date-time + example: "2026-03-28T09:00:00Z" + + LibraryTree: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + documents: + type: array + items: + type: object + properties: + id: + type: string + title: + type: string + subLibraries: + type: array + items: + $ref: '#/components/schemas/LibraryTree' diff --git a/cli-protocol.md b/cli-protocol.md new file mode 100644 index 0000000..926d20c --- /dev/null +++ b/cli-protocol.md @@ -0,0 +1,274 @@ +# SimpleNote CLI - Protocolo de Comunicación + +## 1. Overview + +El CLI (`simplenote-cli`) se comunica con el servidor (`simplenote-web`) exclusivamente +vía HTTP REST usando el API documentado en `api-spec.yaml`. + +No hay comunicación peer-to-peer ni protocolos binarios. Todo es JSON sobre HTTP. + +``` +┌──────────────────┐ HTTP/REST ┌─────────────────┐ +│ simplenote-cli │ ◄──────────────────► │ simplenote-web │ +│ (Commander.js) │ Bearer token auth │ (Express.js) │ +└──────────────────┘ └─────────────────┘ + │ │ + └──── ~/.config/simplenote/config.json ──┘ +``` + +## 2. Cliente HTTP + +### 2.1 Clase `SimpleNoteClient` + +```javascript +// src/api/client.js +const axios = require('axios'); + +class SimpleNoteClient { + constructor({ baseUrl, token }) { + this.baseUrl = baseUrl.replace(/\/$/, ''); // strip trailing slash + this.token = token; + this._axios = axios.create({ + baseURL: this.baseUrl, + timeout: 10000, + headers: { 'Content-Type': 'application/json' } + }); + } + + _authHeaders() { + return this.token ? { Authorization: `Bearer ${this.token}` } : {}; + } + + async _request(method, path, data) { + try { + const res = await this._axios.request({ + method, + url: path, + data, + headers: this._authHeaders() + }); + return res.data; + } catch (err) { + if (err.response) { + const msg = err.response.data?.error || err.message; + throw new Error(`API Error ${err.response.status}: ${msg}`); + } + throw err; + } + } + + // Auth + async verifyToken() { return this._request('GET', '/auth/verify'); } + + // Documents + async listDocuments(params) { + const qs = new URLSearchParams(params).toString(); + return this._request('GET', `/documents${qs ? '?' + qs : ''}`); + } + async getDocument(id) { return this._request('GET', `/documents/${id}`); } + async createDocument(data) { return this._request('POST', '/documents', data); } + async updateDocument(id, data) { return this._request('PUT', `/documents/${id}`, data); } + async deleteDocument(id) { return this._request('DELETE', `/documents/${id}`); } + async exportDocument(id) { return this._request('GET', `/documents/${id}/export`); } + + // Documents > Tags + async addTagsToDocument(id, tags) { + return this._request('POST', `/documents/${id}/tags`, { tags }); + } + + // Libraries + async listLibraries() { return this._request('GET', '/libraries'); } + async getLibrary(id) { return this._request('GET', `/libraries/${id}`); } + async createLibrary(data) { return this._request('POST', '/libraries', data); } + async getLibraryTree(id) { return this._request('GET', `/libraries/${id}/tree`); } + async listLibraryDocuments(id) { + return this._request('GET', `/libraries/${id}/documents`); + } + + // Tags + async listTags() { return this._request('GET', '/tags'); } + async getTagDocuments(tag) { return this._request('GET', `/tags/${tag}`); } +} +``` + +## 3. Flujo de Auth + +### 3.1 Login Inicial + +```bash +simplenote auth login snk_a1b2c3d4e5f6... +``` + +Flujo: +1. CLI guarda token en `~/.config/simplenote/config.json` +2. CLI llama `GET /api/v1/auth/verify` para validar +3. Si 200 → login exitoso. Si 401 → token inválido. + +### 3.2 Requests Subsecuentes + +Todas las requests incluyen: +``` +Authorization: Bearer +``` + +### 3.3 Verificación de Status + +```bash +simplenote auth status +``` +→ `GET /auth/verify` → muestra si token es válido. + +## 4. Comandos CLI Detallados + +### 4.1 Documents + +```bash +# Listar con filtros +simplenote doc list --tag backend --library 550e8400... --type requirement +simplenote doc list --tag api --limit 10 + +# Ver documento +simplenote doc get 770e8400-e29b-41d4-a716-446655440002 + +# Crear +simplenote doc create \ + --title "API Authentication" \ + --content "# API Authentication\n\n..." \ + --tags "backend,api,auth" \ + --type requirement \ + --priority high \ + --library 550e8400-e29b-41d4-a716-446655440000 + +# Actualizar +simplenote doc update 770e8400... --title "Nuevo título" --content "..." +simplenote doc update 770e8400... --status approved + +# Eliminar +simplenote doc delete 770e8400... + +# Exportar como markdown +simplenote doc export 770e8400... + +# Agregar tags +simplenote doc add-tags 770e8400... --tags "new-tag,another" +``` + +### 4.2 Libraries + +```bash +# Listar librerías raíz +simplenote lib list + +# Listar con padre +simplenote lib list --parent 550e8400... + +# Ver contenido +simplenote lib get 550e8400... + +# Crear +simplenote lib create --name "API Specs" +simplenote lib create --name "Sub Librería" --parent 550e8400... + +# Ver árbol completo +simplenote lib tree 550e8400... +``` + +### 4.3 Tags + +```bash +# Listar todos los tags +simplenote tag list + +# Ver docs con tag +simplenote tag docs backend +``` + +### 4.4 Auth + +```bash +# Login con token +simplenote auth login snk_xxxxx + +# Verificar status +simplenote auth status +``` + +## 5. Manejo de Errores + +```javascript +// Errores de API se transforman en mensajes claros +try { + await client.getDocument('non-existent-id'); +} catch (err) { + console.error(err.message); + // "API Error 404: Document not found" +} +``` + +Códigos de error CLI: +- `1` — Error general (network, parse, etc) +- `2` — Token inválido / auth fallida +- `3` — Recurso no encontrado (404) +- `4` — Validación de input + +## 6. Configuración de Conexión + +```javascript +// ~/.config/simplenote/config.json +{ + "apiUrl": "http://localhost:3000/api/v1", + "token": "snk_xxxxx", + "activeLibrary": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +Override por línea de comandos: +```bash +simplenote --api-url http://custom:3000/api/v1 doc list +``` + +## 7. Dependencias CLI + +```json +// package.json +{ + "dependencies": { + "commander": "^11.1.0", + "axios": "^1.6.0", + "chalk": "^5.3.0", + "inquirer": "^9.2.0" + } +} +``` + +## 8. Ejemplo de Sesión Completa + +```bash +$ simplenote auth login snk_a1b2c3d4e5f6 +✓ Token verified. Logged in. + +$ simplenote lib list +[ + { "id": "550e8400...", "name": "Backend Requirements", "documentCount": 5 } +] + +$ simplenote doc create \ + --title "Token Auth" \ + --tags "backend,auth" \ + --type requirement \ + --library 550e8400... +{ + "id": "770e8400...", + "title": "Token Auth", + "tags": ["backend", "auth"], + ... +} + +$ simplenote tag docs backend +[ + { "id": "770e8400...", "title": "Token Auth", ... } +] + +$ simplenote doc get 770e8400... +# Muestra documento formateado con content + metadata +``` diff --git a/data-format.md b/data-format.md new file mode 100644 index 0000000..0c019ec --- /dev/null +++ b/data-format.md @@ -0,0 +1,345 @@ +# SimpleNote - Formato de Datos + +## 1. Archivos JSON de Metadata + +Todos los archivos de metadata son JSON planos, almacenados junto a su contenido en el filesystem. + +--- + +## 2. `.library.json` + +Define una librería (equivalente a una carpeta/directorio). + +**Ubicación**: `data/libraries/{library-uuid}/.library.json` + +**Schema**: +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["id", "name", "path", "createdAt", "updatedAt"], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "UUID único de la librería" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Nombre visible de la librería" + }, + "parentId": { + "type": ["string", "null"], + "format": "uuid", + "description": "ID del padre directo. Null para root." + }, + "path": { + "type": "string", + "description": "Ruta relativa desde DATA_ROOT (ej: libraries/uuid)" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp de creación ISO8601" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp de última modificación ISO8601" + } + } +} +``` + +**Ejemplo**: +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Backend Requirements", + "parentId": null, + "path": "libraries/550e8400", + "createdAt": "2026-03-28T09:00:00Z", + "updatedAt": "2026-03-28T09:00:00Z" +} +``` + +**Ejemplo anidado**: +```json +{ + "id": "660e8400-e29b-41d4-a716-446655440001", + "name": "API Specs", + "parentId": "550e8400-e29b-41d4-a716-446655440000", + "path": "libraries/550e8400/sub-libraries/660e8400", + "createdAt": "2026-03-28T10:00:00Z", + "updatedAt": "2026-03-28T10:00:00Z" +} +``` + +--- + +## 3. `.meta.json` + +Metadata de un documento individual. + +**Ubicación**: `data/libraries/{lib-uuid}/documents/{doc-uuid}/.meta.json` + +**Schema**: +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["id", "title", "tags", "type", "libraryId", "createdAt", "updatedAt"], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + }, + "type": { + "type": "string", + "enum": ["requirement", "note", "spec", "general"] + }, + "status": { + "type": "string", + "enum": ["draft", "approved", "implemented"] + }, + "priority": { + "type": "string", + "enum": ["high", "medium", "low"] + }, + "libraryId": { + "type": "string", + "format": "uuid" + }, + "createdBy": { + "type": "string", + "description": "Agent ID o user ID que creó el documento" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } +} +``` + +**Ejemplo**: +```json +{ + "id": "770e8400-e29b-41d4-a716-446655440002", + "title": "API Authentication Design", + "tags": ["backend", "api", "auth"], + "type": "requirement", + "status": "draft", + "priority": "high", + "libraryId": "550e8400-e29b-41d4-a716-446655440000", + "createdBy": "agent-001", + "createdAt": "2026-03-28T10:00:00Z", + "updatedAt": "2026-03-28T10:00:00Z" +} +``` + +--- + +## 4. `.tag-index.json` + +Índice global de tags. Rebuild completo o incremental. + +**Ubicación**: `data/.tag-index.json` + +**Schema**: +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["version", "updatedAt", "tags"], + "properties": { + "version": { + "type": "integer", + "const": 1, + "description": "Versión del formato de índice" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "Última rebuild del índice" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "type": "string", "format": "uuid" }, + "uniqueItems": true + }, + "description": "Map tag → array de document IDs" + } + } +} +``` + +**Ejemplo**: +```json +{ + "version": 1, + "updatedAt": "2026-03-28T12:00:00Z", + "tags": { + "backend": [ + "770e8400-e29b-41d4-a716-446655440002", + "880e8400-e29b-41d4-a716-446655440003" + ], + "api": [ + "770e8400-e29b-41d4-a716-446655440002" + ], + "auth": [ + "770e8400-e29b-41d4-a716-446655440002", + "990e8400-e29b-41d4-a716-446655440004" + ] + } +} +``` + +--- + +## 5. `.auth-tokens.json` + +Tokens de API válidos. + +**Ubicación**: `data/.auth-tokens.json` + +**Schema**: +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["version", "tokens"], + "properties": { + "version": { + "type": "integer", + "const": 1 + }, + "tokens": { + "type": "array", + "items": { + "type": "object", + "required": ["token", "label", "createdAt"], + "properties": { + "token": { + "type": "string", + "pattern": "^snk_" + }, + "label": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + } + } + } +} +``` + +**Ejemplo**: +```json +{ + "version": 1, + "tokens": [ + { + "token": "snk_a1b2c3d4e5f6g7h8i9j0", + "label": "cli-default", + "createdAt": "2026-03-28T00:00:00Z" + }, + { + "token": "snk_k9j8i7h6g5f4e3d2c1b", + "label": "hiro-workstation", + "createdAt": "2026-03-28T01:00:00Z" + } + ] +} +``` + +--- + +## 6. Documento Markdown (`index.md`) + +Archivo de contenido con frontmatter YAML. + +### 6.1 Frontmatter + +```yaml +--- +id: REQ-001 +title: Título del Requerimiento +type: requirement # requirement | note | spec | general +priority: high # high | medium | low +status: draft # draft | approved | implemented +tags: [backend, api] +createdBy: agent-001 +createdAt: 2026-03-28 +--- +``` + +### 6.2 Body + +Markdown standard con headers, listas, código, etc. + +### 6.3 Ejemplo Completo + +```markdown +--- +id: REQ-001 +title: API Authentication Design +type: requirement +priority: high +status: draft +tags: [backend, api, auth] +createdBy: agent-001 +createdAt: 2026-03-28 +--- + +# API Authentication Design + +## Descripción +El sistema debe soportar autenticación via tokens Bearer para todas las rutas +protegidas bajo `/api/v1/*` excepto `/api/v1/auth/token`. + +## Criterios de Aceptación +- [ ] Endpoint POST /api/v1/auth/token acepta credenciales y retorna token +- [ ] Middleware extrae token del header `Authorization: Bearer ` +- [ ] Middleware retorna 401 si header ausente o token inválido +- [ ] Token tiene prefijo `snk_` para identificación fácil + +## Notas +Tokens en esta versión son secretos compartidos. Para producción se recomienda +OAuth2 o JWT firmados. +``` + +--- + +## 7. Tabla Resumen de Archivos + +| Archivo | Ubicación | Propósito | +|---------|-----------|-----------| +| `.library.json` | `libraries/{id}/` | Metadata de librería | +| `.meta.json` | `libraries/{lib}/documents/{id}/` | Metadata de documento | +| `index.md` | `libraries/{lib}/documents/{id}/` | Contenido del documento | +| `.tag-index.json` | `DATA_ROOT/` | Índice global tag → docs | +| `.auth-tokens.json` | `DATA_ROOT/` | Tokens API válidos | +| `config.json` | `~/.config/simplenote/` | Config local del CLI | diff --git a/env-template.md b/env-template.md new file mode 100644 index 0000000..ba29663 --- /dev/null +++ b/env-template.md @@ -0,0 +1,116 @@ +# SimpleNote - Variables de Entorno + +## Template: `.env.example` + +Copia este archivo a `.env` en la raíz de `simplenote-web/`. + +```env +# ============ SERVER ============ +PORT=3000 +HOST=0.0.0.0 + +# ============ DATA ============ +# Raíz donde se almacenan documentos y archivos de índice +# Default: ./data (relativo al proyecto) +DATA_ROOT=./data + +# ============ AUTH ============ +# Token admin inicial (generado en setup). No exponer en cliente. +ADMIN_TOKEN=snk_initial_admin_token_change_me + +# ============ LOGGING ============ +LOG_LEVEL=info +# Opciones: trace, debug, info, warn, error, fatal + +# ============ CORS ============ +# Orígenes permitidos para requests cross-origin +# Default: * (permitir todos) +CORS_ORIGIN=* + +# Para desarrollo local: +# CORS_ORIGIN=http://localhost:5173 + +# Para producción: +# CORS_ORIGIN=https://simplenote.example.com + +# ============ API ============ +# Versión del API en URLs +API_PREFIX=/api/v1 + +# ============ RATE LIMITING (opcional) ============ +# Requests por minuto por IP +# RATE_LIMIT_ENABLED=true +# RATE_LIMIT_WINDOW_MS=60000 +# RATE_LIMIT_MAX=100 +``` + +## Detalle de Variables + +### `PORT` +- **Tipo**: integer +- **Default**: `3000` +- **Descripción**: Puerto TCP donde Express escucha conexiones entrantes. + +### `HOST` +- **Tipo**: string +- **Default**: `0.0.0.0` +- **Descripción**: Host de binding. `0.0.0.0` = todas las interfaces. + +### `DATA_ROOT` +- **Tipo**: string (ruta) +- **Default**: `./data` +- **Descripción**: Directorio raíz donde se guardan: + - `libraries/` — estructura de carpetas con documentos + - `.tag-index.json` — índice global de tags + - `.auth-tokens.json` — tokens de API + +### `LOG_LEVEL` +- **Tipo**: enum +- **Default**: `info` +- **Opciones**: `trace`, `debug`, `info`, `warn`, `error`, `fatal` +- **Descripción**: Nivel mínimo de logs que se emiten. + +### `CORS_ORIGIN` +- **Tipo**: string +- **Default**: `*` +- **Descripción**: Valor del header `Access-Control-Allow-Origin`. Usar dominio + específico en producción para seguridad. + +### `API_PREFIX` +- **Tipo**: string +- **Default**: `/api/v1` +- **Descripción**: Prefijo para todas las rutas del API. Cambiar permite versionado. + +--- + +## CLI Config: `~/.config/simplenote/config.json` + +```json +{ + "apiUrl": "http://localhost:3000/api/v1", + "token": "snk_your_token_here", + "activeLibrary": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +| Campo | Descripción | +|-------|-------------| +| `apiUrl` | URL base del API (sin trailing slash) | +| `token` | Token Bearer para autenticación | +| `activeLibrary` | ID de librería activa por defecto | + +--- + +## Flags de Línea de Comandos (CLI) + +```bash +# Override apiUrl +simplenote --api-url http://custom:3000/api/v1 doc list + +# Modo verbose +simplenote --verbose doc list + +# Help +simplenote help +simplenote doc help create +``` diff --git a/folder-structure.md b/folder-structure.md new file mode 100644 index 0000000..0a07252 --- /dev/null +++ b/folder-structure.md @@ -0,0 +1,170 @@ +# SimpleNote - Estructura de Carpetas + +## Estructura General del Repositorio + +``` +simplenote-web/ +├── src/ +│ ├── index.js # Entry point (bind port, listen) +│ ├── app.js # Express app setup, middleware, routes +│ ├── config/ +│ │ └── index.js # Env vars loader y defaults +│ ├── routes/ +│ │ ├── index.js # Router principal (mount /api/v1/*) +│ │ ├── documents.js # /documents routes +│ │ ├── libraries.js # /libraries routes +│ │ ├── tags.js # /tags routes +│ │ └── auth.js # /auth routes +│ ├── services/ +│ │ ├── documentService.js # Lógica de documentos +│ │ ├── libraryService.js # Lógica de librerías +│ │ └── tagService.js # Lógica de tags e indexación +│ ├── middleware/ +│ │ ├── auth.js # Middleware de autenticación Bearer +│ │ └── errorHandler.js # Handler global de errores +│ ├── utils/ +│ │ ├── markdown.js # Parseo y serialización de Markdown +│ │ ├── fsHelper.js # Helpers de filesystem (safe paths, etc) +│ │ ├── uuid.js # Wrapper de uuid/v4 +│ │ └── errors.js # Clases de errores custom (NotFound, etc) +│ └── indexers/ +│ └── tagIndexer.js # Rebuild y query del .tag-index.json +├── data/ # Raíz de documentos (DATA_ROOT) +│ ├── .tag-index.json # Índice global de tags +│ ├── .auth-tokens.json # Tokens de API válidos +│ └── libraries/ +│ └── {uuid}/ +│ ├── .library.json +│ ├── documents/ +│ │ └── {uuid}/ +│ │ ├── index.md +│ │ └── .meta.json +│ └── sub-libraries/ +│ └── {child-uuid}/... +├── tests/ +│ ├── unit/ +│ │ ├── services/ +│ │ │ ├── documentService.test.js +│ │ │ ├── libraryService.test.js +│ │ │ └── tagService.test.js +│ │ └── utils/ +│ │ └── markdown.test.js +│ └── integration/ +│ └── api.test.js +├── scripts/ +│ └── init-data.js # Script de inicialización (crea default lib) +├── package.json +├── .env.example +└── README.md + +simplenote-cli/ +├── src/ +│ ├── index.js # Entry point (Commander program) +│ ├── api/ +│ │ └── client.js # SimpleNoteClient (axios-based) +│ ├── commands/ +│ │ ├── index.js # Registra todos los subcommands +│ │ ├── doc.js # simplenote doc +│ │ ├── lib.js # simplenote lib +│ │ └── tag.js # simplenote tag +│ └── config/ +│ ├── loader.js # Carga ~/.config/simplenote/config.json +│ └── errors.js # Errores CLI custom +├── package.json +└── README.md +``` + +## Estructura de Datos en Disco (DATA_ROOT) + +``` +data/ # DATA_ROOT (default: ./data) +├── .tag-index.json # Tag index global +├── .auth-tokens.json # Auth tokens +└── libraries/ + └── {library-uuid}/ + ├── .library.json + ├── documents/ + │ └── {doc-uuid}/ + │ ├── index.md # Contenido Markdown + │ └── .meta.json # Metadata (tags, timestamps) + └── sub-libraries/ + └── {child-uuid}/ + ├── .library.json + └── documents/ + └── ... (recursivo) +``` + +## Archivos de Metadata + +### `.library.json` (por librería) +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Backend Requirements", + "parentId": null, + "path": "libraries/550e8400", + "createdAt": "2026-03-28T09:00:00Z", + "updatedAt": "2026-03-28T09:00:00Z" +} +``` + +### `.meta.json` (por documento) +```json +{ + "id": "550e8401-e29b-41d4-a716-446655440001", + "title": "API Authentication", + "tags": ["backend", "api", "auth"], + "type": "requirement", + "status": "draft", + "priority": "high", + "libraryId": "550e8400-e29b-41d4-a716-446655440000", + "createdBy": "agent-001", + "createdAt": "2026-03-28T10:00:00Z", + "updatedAt": "2026-03-28T10:00:00Z" +} +``` + +### `index.md` (contenido) +```markdown +--- +id: REQ-001 +title: API Authentication +type: requirement +priority: high +status: draft +tags: [backend, api, auth] +createdBy: agent-001 +createdAt: 2026-03-28 +--- + +# API Authentication + +## Descripción +El sistema debe soportar autenticación via tokens Bearer. + +## Criterios de Aceptación +- [ ] Endpoint POST /api/auth/token +- [ ] Middleware valida Bearer token +- [ ] Retorna 401 si token inválido +``` + +## Archivos de Configuración Local (CLI) + +``` +~/.config/simplenote/ +└── config.json # Config CLI local + { + "apiUrl": "http://localhost:3000/api/v1", + "token": "snk_xxxxx", + "activeLibrary": "default" + } +``` + +## Archivos de Configuración del Servidor + +``` +simplenote-web/ +├── .env.example # Template de variables de entorno +├── .env # No commitear (contiene secrets) +└── .gitignore # Ignora .env, data/, node_modules/ +```