Compare commits

..
16 Commits
Author SHA1 Message Date
cupcakearmy f6ea6376e1 Merge pull request #221 from cupcakearmy/fix/spa-fallback-status-code
Fix/spa fallback status code
2026-09-26 13:33:33 +02:00
cupcakearmy 19e7899309 docs: add changelog note for SPA fallback + api 404 fix 2026-09-21 21:57:48 +02:00
cupcakearmy 9e69730bfd fix: keep SPA fallback for non-api paths but 404 unmatched /api routes 2026-09-21 21:39:09 +02:00
cupcakearmy b5e1c4b42a chore: cleanup 2026-09-21 21:27:50 +02:00
cupcakearmy 7660dd7c30 remove uncessary commens 2026-09-21 21:18:59 +02:00
cupcakearmy 685784c360 Merge branch 'main' into fix/spa-fallback-status-code 2026-09-21 21:16:57 +02:00
cupcakearmy cc2c45221b Merge pull request #219 from cupcakearmy/v3
v3
2026-09-21 21:08:14 +02:00
cupcakearmy b113d253a9 chore: bump to 3.0.0 2026-09-21 20:51:51 +02:00
cupcakearmy c99c62cf00 chore: clean up compose file 2026-09-21 20:50:48 +02:00
cupcakearmy 7ef162bd83 chore: bump to 3.0.0-rc.4 2026-09-19 16:02:55 +02:00
cupcakearmy 282fdb97d1 docs: add LLM usage disclosure to readmes 2026-09-19 16:00:04 +02:00
cupcakearmy 4dfed492bc docs: correct frontend bullet in v3 changelog (was already SvelteKit) 2026-09-19 15:51:37 +02:00
cupcakearmy 5084ed59f0 docs: fix incorrect pg note wording in v3 changelog 2026-09-19 15:47:20 +02:00
cupcakearmy 055a48a38d fix(web): stop transferring nested file buffers in worker unpack (comlink multi-buffer failure) 2026-09-19 15:13:34 +02:00
cupcakearmy d1126ace82 fix(web): don't run file downloads as a $derived side effect 2026-09-19 14:44:35 +02:00
Matheus Leal 8034b1a501 fix: serve SPA fallback with 200 instead of 404
`ServeDir::not_found_service` wraps the fallback in `SetStatus`, which forces
every response to `404 Not Found`. Client side routes such as `/note/<id>` and
`/about` were therefore served the correct `index.html` but with a 404 status.

Use `ServeDir::fallback` instead, which leaves the status untouched. A note that
genuinely does not exist is still reported as 404 by `/api/notes/<id>`.

Behind a reverse proxy this made effectively every document request show up as a
4xx, skewing error rate dashboards and triggering false alerts.

Fixes #217
2026-09-03 16:09:40 -03:00
12 changed files with 59 additions and 28 deletions
+6 -2
View File
@@ -11,15 +11,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New shared TypeScript package `@cryptgeon/shared` as single source of truth for crypto, content codec and API client (crypto + compression + payload + types).
- Shared payload codec: `packContent` / `unpackContent` (encode → LZ4 → XChaCha20-Poly1305 and reverse).
- New `pg`-backend storage of note hashes in the cache.
- Cache-backed note storage using hashes (valkey/redis) with atomic view counting.
### Changed
- Encryption from AES to **XChaCha20-Poly1305** (client-side); dropped `occulto`.
- All API bodies switched to **MessagePack**.
- Frontend migrated to SvelteKit + `@cryptgeon/shared`.
- Frontend uses `@cryptgeon/shared` for crypto + payload codec (replacing the previous local `cryptgeon/shared`); heavy pack/unpack runs in a web worker.
- CLI rebuilt with `vite-plus` (bundles all deps) and imports from `@cryptgeon/shared`.
### Fixed
- SPA fallback now serves the app (200) for client-side routes such as `/about` and `/note/<id>`, while unmatched `/api/*` paths still return 404.
### Breaking changes
- Endpoints moved to `/api/v3/notes/` and `/api/v3/status`; health check to `/healthz`.
+4
View File
@@ -169,6 +169,10 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md).
Please refer to the security section [here](./SECURITY.md).
## Usage of LLMs
Starting from V3, I used LLMs heavily to implement _my own ideas_. This means that the direction and architecture choices are human. A lot of the implementation that derives from that, is automated with an LLM.
---
_Attributions_
+4
View File
@@ -151,6 +151,10 @@ Ver [CONTRIBUTING.md](./CONTRIBUTING.md).
Por favor dirígete a la sección de seguridad [aquí](./SECURITY.md).
## Uso de LLMs
A partir de la V3, utilicé LLMs de forma intensiva para implementar _mis propias ideas_. Esto significa que la dirección y las decisiones de arquitectura son humanas. Gran parte de la implementación que deriva de eso está automatizada con un LLM.
---
_Atribuciones_
+4
View File
@@ -149,6 +149,10 @@ services:
参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
## LLM 的使用
从 V3 开始,我大量使用 LLM 来实现_我自己的想法_。这意味着项目的方向和架构选择均由人类决定。由此衍生的大部分实现由 LLM 自动完成。
###### Attributions
- 测试数据:
+10 -12
View File
@@ -1,17 +1,17 @@
services:
cache:
image: valkey/valkey:7-alpine
image: valkey/valkey:9-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
# https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# https://valkey.io/topics/lru-cache/
# --maxmemory 1gb --maxmemory-policy allkeys-lru
# This prevents the creation of an anonymous volume.
tmpfs:
- /data
app:
image: cupcakearmy/cryptgeon:v3
image: cupcakearmy/cryptgeon:3
depends_on:
- cache
environment:
@@ -19,11 +19,9 @@ services:
SIZE_LIMIT: 4 MiB
ports:
- 80:8000
# Optional health checks
# healthcheck:
# test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"]
# interval: 1m
# timeout: 3s
# retries: 2
# start_period: 5s
healthcheck:
test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"]
interval: 1m
timeout: 3s
retries: 2
start_period: 5s
+1 -1
View File
@@ -252,7 +252,7 @@ dependencies = [
[[package]]
name = "cryptgeon"
version = "3.0.0-rc.3"
version = "3.0.0"
dependencies = [
"axum",
"bs62",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "cryptgeon"
version = "3.0.0-rc.3"
version = "3.0.0"
authors = ["cupcakearmy <hi@nicco.io>"]
edition = "2024"
rust-version = "1.95"
+9 -3
View File
@@ -20,6 +20,10 @@ mod note;
mod status;
mod store;
async fn api_not_found() -> axum::http::StatusCode {
axum::http::StatusCode::NOT_FOUND
}
#[tokio::main]
async fn main() {
dotenv().ok();
@@ -39,11 +43,13 @@ async fn main() {
.nest("/notes", notes_routes)
.merge(status_routes);
let api_routes = Router::new().nest("/v3", v3_routes);
let api_routes = Router::new()
.nest("/v3", v3_routes)
.fallback(api_not_found);
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
ServeDir::new(config::FRONTEND_PATH.to_string()).fallback(ServeFile::new(index));
let app = Router::new()
.nest("/api", api_routes)
.merge(health_routes)
@@ -66,4 +72,4 @@ async fn main() {
axum::serve(listener, ServiceExt::<Request>::into_make_service(app))
.await
.unwrap();
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "cryptgeon",
"version": "3.0.0-rc.3",
"version": "3.0.0",
"homepage": "https://github.com/cupcakearmy/cryptgeon",
"repository": {
"type": "git",
+3 -3
View File
@@ -46,11 +46,11 @@ async function downloadFile(file: FileDTO) {
files = note.contents
}
})
let download = $derived(() => {
function downloadAll() {
for (const file of files) {
downloadFile(file)
}
})
}
let links = $derived(typeof note.contents === 'string' ? note.contents.match(RE_URL) : [])
</script>
@@ -92,7 +92,7 @@ async function downloadFile(file: FileDTO) {
{/key}
{/if}
{/each}
<Button onclick={download}>{$t('show.download_all')}</Button>
<Button onclick={downloadAll}>{$t('show.download_all')}</Button>
{/if}
</div>
+1 -5
View File
@@ -8,11 +8,7 @@ const contract: WorkerConract = {
return transfer(content, [content.data.buffer, content.extra.buffer, content.key.buffer])
},
unpack(data, key) {
const content = unpackContent(data, key)
return transfer(
content,
typeof content.data === 'string' ? [] : content.data.map((f) => f.data.buffer)
)
return unpackContent(data, key)
},
}
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
test.describe("@web", () => {
for (const path of ["/", "/about", "/note/does-not-exist"]) {
test(`serves ${path} with status 200`, async ({ request }) => {
const response = await request.get(path);
expect(response.status()).toBe(200);
});
}
test("api still reports a missing note as 404", async ({ request }) => {
const response = await request.get("/api/notes/does-not-exist");
expect(response.status()).toBe(404);
});
});