Compare commits
No commits in common. "master" and "issue-71-widget-expand-perf" have entirely different histories.
master
...
issue-71-w
12 changed files with 319 additions and 612 deletions
|
|
@ -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
|
||||
|
|
|
|||
50
SECURITY.md
50
SECURITY.md
|
|
@ -1,50 +0,0 @@
|
|||
# SECURITY — simpl-liste
|
||||
|
||||
Rapport d'etat securite. Scanne quotidiennement par `defenseur-simpl-liste`
|
||||
(04:15 UTC) dans `~/claude-code/defenseurs/`.
|
||||
|
||||
## CVE resolues
|
||||
|
||||
### 2026-04-24 — Overrides `@xmldom/xmldom` et `uuid`
|
||||
|
||||
Correction via `overrides` dans `package.json`. Aucune n'etait exploitable a
|
||||
runtime dans l'APK : chaine concernee est build-time (iOS build via xcode,
|
||||
Expo CLI, plist serialization) ou dev-tunnel (ngrok).
|
||||
|
||||
| CVE | Severite | Package | Fix version | Etat |
|
||||
|---|---|---|---|---|
|
||||
| GHSA-2v35-w6hq-6mfw | HIGH | @xmldom/xmldom | ^0.8.13 | Resolu |
|
||||
| GHSA-f6ww-3ggp-fr8h | HIGH | @xmldom/xmldom | ^0.8.13 | Resolu |
|
||||
| GHSA-x6wf-f3px-wcqx | HIGH | @xmldom/xmldom | ^0.8.13 | Resolu |
|
||||
| GHSA-j759-j44w-7fr8 | HIGH | @xmldom/xmldom | ^0.8.13 | Resolu |
|
||||
| GHSA-w5hq-g745-h8pq | MEDIUM | uuid | ^11.0.0 | Mitigation partielle (voir ci-dessous) |
|
||||
|
||||
## CVE residuelles
|
||||
|
||||
### GHSA-w5hq-g745-h8pq — `uuid` buffer bounds check
|
||||
|
||||
**Status** : mitigation partielle. L'advisory npm flag `uuid <14.0.0` meme apres
|
||||
override a `^11.0.0`. Bug concerne `uuid.v3() / v5() / v6()` quand `buf` param
|
||||
est fourni. Les consommateurs transitifs (`xcode`, `@expo/ngrok`) utilisent
|
||||
uniquement `uuid.v4()` — donc **pas de code path vulnerable atteint en pratique**.
|
||||
|
||||
Non bump vers `^14.0.0` car ESM-only (casserait les imports CJS de xcode et
|
||||
ngrok dans la chaine build iOS). Voir spec decision D3 pour details.
|
||||
|
||||
**Impact** : cascade via 18 advisories transitives (uuid → xcode/ngrok → expo/* → ...),
|
||||
toutes remontant a la meme racine. Non-bloquant pour la production.
|
||||
|
||||
**Re-evaluation** : lors du prochain upgrade Expo SDK (quand xcode + ngrok
|
||||
passeront a une version compatible avec `uuid@^14` ou retireront uuid).
|
||||
|
||||
## Procedure de scan manuel
|
||||
|
||||
```bash
|
||||
cd ~/claude-code/defenseurs && npx tsx src/defenseur.ts defenseur-simpl-liste
|
||||
```
|
||||
|
||||
## Politique de review
|
||||
|
||||
- **HIGH** : fix immediat via PR.
|
||||
- **MEDIUM** : triage sous 7 jours, fix si exploitable.
|
||||
- **LOW** : accepte tel quel, documente ici.
|
||||
29
STATE.md
29
STATE.md
|
|
@ -1,29 +0,0 @@
|
|||
# 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
|
||||
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.
|
||||
4
app.json
4
app.json
|
|
@ -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",
|
||||
|
|
|
|||
635
package-lock.json
generated
635
package-lock.json
generated
File diff suppressed because it is too large
Load diff
44
package.json
44
package.json
|
|
@ -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",
|
||||
|
|
@ -65,34 +64,7 @@
|
|||
"typescript": "~5.9.2"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
"esbuild": "^0.25.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
// Build query
|
||||
const query = db
|
||||
let query = db
|
||||
.select()
|
||||
.from(slTasks)
|
||||
.where(and(...conditions));
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue