Compare commits

..

No commits in common. "master" and "fix/vuln-postcss-override" have entirely different histories.

11 changed files with 307 additions and 532 deletions

View file

@ -15,39 +15,10 @@ user-invocable: true
1. Lire le `versionCode` actuel dans `app.json`
2. Incrementer `versionCode` (+1) — doit etre strictement superieur
3. Si demande par l'utilisateur : bumper `version` dans `app.json` + `package.json` (et `npm install` pour synchroniser le lockfile)
4. Commit : `chore: bump versionCode to <N>` (ou `chore: bump version to X.Y.Z (versionCode N)` si version aussi bumpee)
5. Push commit sur master, puis tag `vX.Y.Z` et `git push origin vX.Y.Z`
6. Build : `npx --yes eas-cli build --platform android --profile preview --non-interactive`
7. Quand le build est termine : telecharger l'APK, creer la release Forgejo, attacher l'APK
## Upload de l'APK — pattern resilient
```bash
# 1. Telecharger
APK_URL=$(npx --yes eas-cli build:view <build_id> --json | jq -r '.artifacts.buildUrl')
curl -L -o "/tmp/simpl-liste-vX.Y.Z.apk" "$APK_URL"
# 2. Creer la release
TOKEN=$(cat ~/.forgejo-token)
RELEASE_ID=$(curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"https://git.lacompagniemaximus.com/api/v1/repos/maximus/simpl-liste/releases" \
-d '{"tag_name":"vX.Y.Z","name":"vX.Y.Z","body":"...","draft":false,"prerelease":false}' \
| jq -r '.id')
# 3. Attacher l'APK — RETRY sur 502/504 (variance reseau, pas un probleme infra)
for i in 1 2 3 4 5; do
http=$(curl -X POST -H "Authorization: token $TOKEN" \
"https://git.lacompagniemaximus.com/api/v1/repos/maximus/simpl-liste/releases/$RELEASE_ID/assets?name=simpl-liste-vX.Y.Z.apk" \
-F "attachment=@/tmp/simpl-liste-vX.Y.Z.apk" \
-o /tmp/asset-resp.json -w "%{http_code}" --max-time 300)
case "$http" in
201) echo "Upload OK"; break;;
502|504) echo "Attempt $i: gateway timeout, retry"; sleep 5;;
*) echo "Unexpected: HTTP $http"; cat /tmp/asset-resp.json; break;;
esac
done
```
3. Si demande par l'utilisateur : bumper `version` dans `app.json` + `package.json`
4. Commit : `chore: bump versionCode to <N>`
5. Build : `npx --yes eas-cli build --platform android --profile preview --non-interactive`
6. Quand le build est termine : creer une release Forgejo avec le lien APK
## Regles
@ -55,22 +26,3 @@ done
- `autoIncrement` dans eas.json ne s'applique qu'au profil `production`, pas `preview`
- Toujours utiliser `npx --yes eas-cli` (pas d'install globale)
- Ne JAMAIS `git push --tags` — push les tags un par un si necessaire
## Gotcha — Upload APK : 502/504 transitoire
**Symptome** : `curl -X POST ... -F "attachment=@<apk>"` retourne HTTP 502 ou 504 apres ~60s. La progression curl montre upload coupe a 50-90% de progression. Affecte typiquement les APK ~90MB+.
**Cause** : Traefik (devant Forgejo via Coolify) a un timeout de gateway de 60s sur la fin de requete. Quand la vitesse upload tombe sous ~1.5 MB/s (variance reseau classique sur connexions residentielles), un APK de 92 MB depasse le seuil et la requete est coupee. **Ce n'est pas un probleme de config infra** — c'est purement la vitesse upload du client a un instant donne.
**Fix** : retry. La 2e ou 3e tentative passe quand la bande passante remonte (j'ai mesure 1.3 MB/s puis 17 MB/s a quelques minutes d'intervalle). Le pattern `for i in 1..5` ci-dessus est suffisant — pas besoin de toucher a Traefik ni Forgejo.
**Verification que c'est bien le bon probleme** :
- L'APK precedent (~97 MB) a ete uploade avec succes les semaines passees → infra OK
- Un fichier de test de 11 octets sur la meme release passe en 0.1s → API OK
- HTTP 502 ou 504 apres 60s exactement → timeout gateway, pas erreur de logique
- Vitesse upload curl reportee < 1.5 MB/s confirme la cause
**Path API a connaitre** :
- Creer release : `POST /repos/{owner}/{repo}/releases` (renvoie `.id`)
- Attacher asset : `POST /repos/{owner}/{repo}/releases/{release_id}/assets?name=<filename>` (multipart `attachment`)
- Supprimer asset : `DELETE /repos/{owner}/{repo}/releases/{release_id}/assets/{asset_id}`**inclure le release_id dans le path**, pas seulement l'asset id, sinon HTTP 404

View file

@ -1,29 +1,21 @@
# STATE — simpl-liste
> Derniere MAJ : 2026-05-30 (deploy web liste.lacompagnie + #90)
## Position actuelle
Version 1.6.4 (versionCode 16). Remediation vulnerabilites du defenseur en cours
Version 1.6.1 (versionCode 13). Remediation vulnerabilites du defenseur en cours
via overrides `@xmldom/xmldom@^0.8.13` et `uuid@^11.0.0` dans `package.json`
(overnight 2026-04-24). 4 CVE HIGH xmldom nettoyees ; 1 advisory uuid residuelle
non-exploitable en pratique (details dans `SECURITY.md`).
Companion web `liste.lacompagniemaximus.com` : deploye **manuellement** (docker-compose
standalone `/data/simpl-liste-web` sur le VPS, build depuis `source/` rsync — PAS une app
Coolify malgre la spec). Procedure en memoire `simpl-liste-web-deploy`.
## Decisions recentes
- 2026-05-30 : #90 merged (PR #93, ThemeToggle eslint-disable) + premier deploy web depuis 7 sem (rsync source/ + docker compose --build sur VPS) — #70 et #90 live sur liste.lacompagniemaximus.com.
- 2026-05-30 : PR #89 merged — display name web resolu via userInfo (ordre fallback de la vitrine), fix #70.
- 2026-05-30 : nettoyage lint web/ (PR #91, 2 prefer-const + import inutilise) ; #90 analyse (ThemeToggle set-state-in-effect, decision : eslint-disable documente).
- 2026-05-10 : Aligne 6 patches Expo SDK 54 via expo install --fix — expo-doctor 17/17 (ref #87)
- 2026-05-08 : Defenseur rerun confirme 0 findings — overrides existants suffisent, pas de PR necessaire (ref #81)
- 2026-04-24 : overrides xmldom + uuid (spec `spec-decisions-vuln-simpl-liste.md`) — PRs #77, #78, #79 (pending-human).
- 2026-04-23 : PR #71 merged — fix widget render-optimiste + timing instrumentation.
- 2026-04-18 : archive milestone `spec-simpl-liste-web` (12/12 done).
## Blockers actifs
Aucun.
- Advisory GHSA-w5hq-g745-h8pq (`uuid <14.0.0`) reste flaggee par npm audit
apres override ^11 : non-exploitable (xcode/ngrok utilisent `uuid.v4()`,
bug dans v3/v5/v6+buf). Decision Max requise : accepter le residuel ou
tenter bump ^14 (risque ESM-only).

View file

@ -2,7 +2,7 @@
"expo": {
"name": "Simpl-Liste",
"slug": "simpl-liste",
"version": "1.6.4",
"version": "1.6.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "simplliste",
@ -24,7 +24,7 @@
"backgroundColor": "#FFF8F0"
},
"edgeToEdgeEnabled": true,
"versionCode": 16
"versionCode": 13
},
"plugins": [
"expo-router",

605
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
{
"name": "simpl-liste",
"main": "index.js",
"version": "1.6.4",
"version": "1.6.1",
"scripts": {
"start": "expo start",
"test": "node tests/smoke.test.cjs",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
@ -19,25 +18,25 @@
"@react-navigation/native": "^7.1.8",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.45.2",
"expo": "~54.0.34",
"expo-auth-session": "~7.0.11",
"expo": "~54.0.33",
"expo-auth-session": "~7.0.10",
"expo-calendar": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-crypto": "~15.0.9",
"expo-crypto": "~15.0.8",
"expo-file-system": "~19.0.21",
"expo-font": "~14.0.11",
"expo-haptics": "~15.0.8",
"expo-intent-launcher": "~13.0.8",
"expo-linking": "~8.0.12",
"expo-linking": "~8.0.11",
"expo-localization": "~17.0.8",
"expo-notifications": "~0.32.17",
"expo-notifications": "~0.32.16",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-sharing": "~14.0.8",
"expo-splash-screen": "~31.0.13",
"expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.11",
"expo-web-browser": "~15.0.10",
"i18next": "^25.8.13",
"lucide-react-native": "^0.575.0",
"nativewind": "^4.2.2",
@ -67,32 +66,8 @@
"overrides": {
"esbuild": "^0.25.0",
"@xmldom/xmldom": "^0.8.13",
"uuid": "^11.1.1",
"postcss": "^8.5.10",
"@expo/cli": {
"ws": "^8.20.1",
"undici": "^6.27.0",
"tar": "^7.5.19"
},
"react-devtools-core": {
"shell-quote": "^1.8.4",
"ws": "^7.5.11"
},
"react-native": {
"ws": "^7.5.11"
},
"@react-native/dev-middleware": {
"ws": "^7.5.11"
},
"metro": {
"ws": "^7.5.11"
},
"@expo/xcpretty": {
"js-yaml": "^4.2.0"
},
"@istanbuljs/load-nyc-config": {
"js-yaml": "^3.15.0"
}
"uuid": "^11.0.0",
"postcss": "^8.5.10"
},
"private": true
}

View file

@ -1,85 +0,0 @@
'use strict';
// Non-regression smoke tests for npm overrides + deps integrity.
// Runs on plain node (no jest, no Expo runtime). Catches the obvious
// breakage paths after `npm install` rewrites lock for an `overrides` bump.
//
// node tests/smoke.test.cjs
//
// Exit 0 if all checks pass, 1 if any fails.
const assert = require('node:assert').strict;
let failed = 0;
function check(name, fn) {
try {
fn();
console.log(`OK ${name}`);
} catch (e) {
failed++;
console.error(`FAIL ${name}: ${e.message}`);
}
}
const UUID_RE =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
check('package.json is valid JSON with name + deps', () => {
const pkg = require('../package.json');
assert.ok(pkg.name, 'package.json missing name');
assert.ok(pkg.dependencies, 'package.json missing dependencies');
});
check('uuid v4 generates well-formed UUID', () => {
const { v4 } = require('uuid');
for (let i = 0; i < 5; i++) {
assert.match(v4(), UUID_RE);
}
});
// The uuid <14.0.0 advisory (GHSA-w5hq-g745-h8pq) is specifically about
// missing buffer bounds checks in v3/v5/v6. After bumping to v14, these
// must still produce valid UUIDs from a name + namespace.
check('uuid v3 with namespace produces valid UUID', () => {
const { v3 } = require('uuid');
const id = v3('test-name', NAMESPACE);
assert.match(id, UUID_RE);
});
check('uuid v5 with namespace produces valid UUID', () => {
const { v5 } = require('uuid');
const id = v5('test-name', NAMESPACE);
assert.match(id, UUID_RE);
});
// Buffer-arg path is the actual vuln site — must not throw and must
// fill the buffer with the UUID bytes.
check('uuid v3 with buffer arg fills buffer (vuln site)', () => {
const { v3 } = require('uuid');
const buf = Buffer.alloc(16);
v3('test-name', NAMESPACE, buf);
// After the call, buf must have at least one non-zero byte.
assert.ok(
buf.some((b) => b !== 0),
'buffer was not filled by uuid v3'
);
});
check('uuid v5 with buffer arg fills buffer (vuln site)', () => {
const { v5 } = require('uuid');
const buf = Buffer.alloc(16);
v5('test-name', NAMESPACE, buf);
assert.ok(
buf.some((b) => b !== 0),
'buffer was not filled by uuid v5'
);
});
if (failed === 0) {
console.log('\nsmoke OK');
process.exit(0);
} else {
console.error(`\nsmoke FAIL (${failed} failure${failed > 1 ? 's' : ''})`);
process.exit(1);
}

View file

@ -74,7 +74,7 @@ export default async function ListPage({
// Fetch subtasks for all parent tasks
const parentIds = tasks.map((t) => t.id);
const subtasksMap: Record<string, Task[]> = {};
let subtasksMap: Record<string, Task[]> = {};
if (parentIds.length > 0) {
const allSubtasks = await db

View file

@ -49,7 +49,7 @@ export async function GET(
}
// Build query
const query = db
let query = db
.select()
.from(slTasks)
.where(and(...conditions));

View file

@ -2,6 +2,7 @@
import { ThemeToggle } from "./ThemeToggle";
import { User, LogOut } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useTranslation } from "react-i18next";

View file

@ -12,11 +12,6 @@ export function ThemeToggle() {
useEffect(() => {
const stored = localStorage.getItem("sl-theme") as Theme | null;
// localStorage is unavailable during SSR, so the stored theme can only be
// read post-mount. ThemeScript already applies the `dark` class before
// hydration (no page FOUC); only the toggle icon corrects on mount. The
// rule is a false positive for this hydration-from-localStorage read.
// eslint-disable-next-line react-hooks/set-state-in-effect
if (stored) setTheme(stored);
}, []);

View file

@ -4,26 +4,16 @@ import { logtoConfig } from './logto';
export const getAuthenticatedUser = cache(async () => {
try {
const context = await getLogtoContext(logtoConfig, { fetchUserInfo: true });
const context = await getLogtoContext(logtoConfig);
if (!context.isAuthenticated || !context.claims?.sub) {
return null;
}
const { claims, userInfo } = context;
// Mirror the vitrine's display-name resolution (la-compagnie-maximus#80):
// prefer the userInfo endpoint over ID token claims, falling back to email
// in the layout. The `name` claim is often absent from the ID token while
// present in userInfo, which is why the user saw their email instead of "Max".
return {
userId: claims.sub,
email: userInfo?.email || claims.email,
name:
userInfo?.name ||
userInfo?.username ||
claims.name ||
claims.username,
userId: context.claims.sub,
email: context.claims.email,
name: context.claims.name,
};
} catch (error) {
console.error('[auth] getLogtoContext error:', error);