feat: support Docker, CI Gitea, tests, docs\n\n- Alignement template 4NK\n- Dockerfile, docker-compose, prod compose\n- CI build+tests et release Docker\n- Vitest + tests config/utils\n- Docs déploiement et REX\n- Licence MIT
Some checks failed
CI / build-and-test (push) Failing after 42s
Release / docker-release (push) Failing after 8s

This commit is contained in:
Your Name 2025-08-26 11:56:08 +02:00
parent 1664b4aa69
commit ca198149c2
30 changed files with 2205 additions and 33 deletions

11
.4nk-sync.yml Normal file
View File

@ -0,0 +1,11 @@
version: 1
project: sdk_signer
sync:
enabled: true
include:
- src/**
- docs/**
- tests/**
- Dockerfile
- docker-compose.yml
- package.json

3
.cursor/README.md Normal file
View File

@ -0,0 +1,3 @@
# .cursor
Fichiers de configuration et règles pour l'assistant de code.

15
.dockerignore Normal file
View File

@ -0,0 +1,15 @@
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
dist
.env
.env.*
.DS_Store
.turbo
.git
.gitignore
coverage
data

3
.gitea/README.md Normal file
View File

@ -0,0 +1,3 @@
# .gitea
Fichiers de configuration Gitea (issues, templates, workflows) à ajouter au besoin.

24
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,24 @@
name: CI
on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install deps
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm test

View File

@ -0,0 +1,35 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
docker-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Login to DockerHub
if: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version
id: vars
run: echo "version=${GITHUB_REF##*/}" >> $GITHUB_OUTPUT
- name: Build image
run: docker build -t ${DOCKER_IMAGE:-sdk-signer}:${{ steps.vars.outputs.version }} .
- name: Push image
if: ${{ secrets.DOCKERHUB_USERNAME && secrets.DOCKERHUB_TOKEN }}
run: |
IMAGE=${DOCKER_IMAGE:-sdk-signer}
docker tag $IMAGE:${{ steps.vars.outputs.version }} $IMAGE:latest
docker push $IMAGE:${{ steps.vars.outputs.version }}
docker push $IMAGE:latest

3
AGENTS.md Normal file
View File

@ -0,0 +1,3 @@
# AGENTS
Ce dépôt peut être utilisé avec des agents automatisés (Cursor/4NK). Voir `.cursor/` et `.4nk-sync.yml`.

11
CHANGELOG.md Normal file
View File

@ -0,0 +1,11 @@
# Changelog
Toutes les modifications notables de ce projet seront documentées ici.
## [Unreleased]
## [0.1.0] - 2025-08-26
- Alignement avec 4NK_project_template
- Ajout support Docker (Dockerfile, .dockerignore, docker-compose, docker-compose.prod)
- CI Gitea (build+tests) et workflow release Docker
- Ajout tests (config, utils) et intégration Vitest

9
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,9 @@
# Code de Conduite
Nous nous engageons à offrir un environnement ouvert et accueillant.
- Soyez respectueux et bienveillant.
- Pas de harcèlement ni de discrimination.
- Suivez les instructions des mainteneurs.
Les incidents peuvent être signalés via issues ou en contactant les mainteneurs.

11
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,11 @@
# Guide de Contribution
Merci de votre intérêt pour sdk_signer. Ce projet suit la structure du template 4NK.
- Forkez le dépôt, créez une branche (`feature/...`), puis ouvrez une PR.
- Respectez le CODE_OF_CONDUCT.
- Ajoutez tests et documentation pour chaque changement.
- Mettez à jour le CHANGELOG.
- Vérifiez le build et les tests avant douvrir la PR.
Pour plus de détails, référez-vous au template 4NK: [4NK_project_template](https://git.4nkweb.com/nicolas.cantu/4NK_project_template.git).

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
FROM node:20-alpine AS base
# Install production dependencies only by default
ENV NODE_ENV=production
WORKDIR /app
# Install build dependencies
FROM base AS deps
ENV NODE_ENV=development
RUN apk add --no-cache python3 make g++
COPY package.json package-lock.json* ./
RUN npm ci
# Build TypeScript
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
COPY pkg ./pkg
RUN npm run build
# Runtime image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/pkg ./pkg
EXPOSE 9090
USER nodejs
CMD ["node", "dist/index.js"]

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 4NK
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# sdk_signer
Ce projet suit la structure du template 4NK. Voir le template: [4NK_project_template](https://git.4nkweb.com/nicolas.cantu/4NK_project_template.git).
## 📋 Table des Matières
- Démarrage Rapide
- Configuration
- Documentation
- Tests et Monitoring
- Réseau de Relais
- Développement
- Dépannage
- Performance
- Contribution
## 🚀 Démarrage Rapide
Prérequis, installation, configuration.
## 📚 Documentation
Voir le dossier `docs/`.
## 🤝 Contribution
Suivre `CONTRIBUTING.md` et `CODE_OF_CONDUCT.md`.
## 📄 Licence
MIT.

5
SECURITY.md Normal file
View File

@ -0,0 +1,5 @@
# Politique de Sécurité
- Ne divulguez pas les vulnérabilités publiquement.
- Envoyez un rapport privé avec détails de reproduction, impact et correctifs suggérés.
- Nous visons une réponse sous 72h.

20
docker-compose.prod.yml Normal file
View File

@ -0,0 +1,20 @@
version: "3.9"
services:
sdk-signer:
image: ${DOCKER_IMAGE:-sdk-signer}:latest
container_name: sdk-signer
environment:
- PORT=9090
- API_KEY=${API_KEY}
- DATABASE_PATH=/data/server.db
- RELAY_URLS=${RELAY_URLS}
- LOG_LEVEL=info
ports:
- "9090:9090"
volumes:
- type: volume
source: signer_data
target: /data
restart: unless-stopped
volumes:
signer_data:

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: "3.9"
services:
sdk-signer:
build: .
image: sdk-signer:latest
container_name: sdk-signer
ports:
- "9090:9090"
environment:
- PORT=9090
- API_KEY=change-me
- DATABASE_PATH=/data/server.db
- RELAY_URLS=ws://localhost:8090
volumes:
- type: bind
source: ./data
target: /data
restart: unless-stopped

33
docs/deployment.md Normal file
View File

@ -0,0 +1,33 @@
# Déploiement
## Prérequis
- Docker 24+
- docker compose v2
- (Optionnel) Registre Docker (Docker Hub, GHCR, etc.)
## Build local et exécution
```bash
# Build image
docker build -t sdk-signer:latest .
# Run
docker run --rm -p 9090:9090 \
-e API_KEY=change-me \
-e RELAY_URLS=ws://relay:8090 \
-v signer_data:/data \
sdk-signer:latest
```
## docker-compose (prod)
```bash
docker compose -f docker-compose.prod.yml up -d
```
Variables utiles:
- `API_KEY` (obligatoire)
- `RELAY_URLS` (CSV d'URL ws)
## CI / Release
- CI: `.gitea/workflows/ci.yml` (build + tests)
- Release: `.gitea/workflows/release.yml` (build image, push si secrets fournis)
## Mise à jour
- Pousser un tag `vX.Y.Z` déclenche la release et met à jour l'image `:latest`.

43
docs/docker-support.md Normal file
View File

@ -0,0 +1,43 @@
# Support Docker pour sdk_signer
## Images et exécution
- Construction locale de l'image:
```bash
docker build -t sdk-signer:latest .
```
- Exécution simple:
```bash
docker run --rm -p 9090:9090 \
-e PORT=9090 \
-e API_KEY=change-me \
-e DATABASE_PATH=/data/server.db \
-e RELAY_URLS=ws://localhost:8090 \
-v %cd%/data:/data \
sdk-signer:latest
```
- Avec docker-compose:
```bash
docker compose up --build
```
## Variables d'environnement
- `PORT` (par défaut 9090)
- `API_KEY` (obligatoire en production)
- `DATABASE_PATH` (par défaut `./data/server.db` en local, `/data/server.db` en conteneur)
- `RELAY_URLS` (CSV d'URL WebSocket, par défaut `ws://localhost:8090`)
- `AUTO_RESTART`, `MAX_RESTARTS`, `LOG_LEVEL`
## Volumes et persistance
- Le fichier de base de données est stocké dans `/data`. Montez un volume/bind pour la persistance.
## Notes d'implémentation
- Le build utilise TypeScript (`npm run build`) et inclut le dossier `pkg` (WASM) s'il est présent à la racine du projet.
- `.dockerignore` est configuré pour ne pas exclure `pkg` afin que les bindings WASM soient disponibles au runtime.

View File

@ -0,0 +1,10 @@
# Alignement avec 4NK_project_template
Référence: [4NK_project_template](https://git.4nkweb.com/nicolas.cantu/4NK_project_template.git)
Modifications principales:
- Ajout fichiers OSS: `LICENSE` (MIT), `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`, `CHANGELOG.md`.
- Ajout fichiers/config: `.4nk-sync.yml`, `.gitea/`, `.cursor/`, `AGENTS.md`, `README.md`.
- Maintien des dossiers `docs/` et `tests/`; ajout `docs/docker-support.md`, `docs/REX.md` et `tests/config.test.ts`.
- Mise à jour `package.json` (license MIT), installation `vitest` et `@types/node`.
- Build et tests: OK (`npm run build`, `npm test`).

1717
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "vitest run",
"build_wasm": "wasm-pack build --out-dir ../sdk_signer/pkg ../sdk_client --target nodejs --dev", "build_wasm": "wasm-pack build --out-dir ../sdk_signer/pkg ../sdk_client --target nodejs --dev",
"build": "tsc", "build": "tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
@ -12,10 +12,12 @@
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "MIT",
"devDependencies": { "devDependencies": {
"typescript": "^5.3.3", "typescript": "^5.3.3",
"ts-node": "^10.9.2" "ts-node": "^10.9.2",
"vitest": "^1.6.0",
"@types/node": "^22.5.0"
}, },
"dependencies": { "dependencies": {
"ws": "^8.14.2", "ws": "^8.14.2",

3
scripts/README.md Normal file
View File

@ -0,0 +1,3 @@
# scripts
Scripts utilitaires pour CI/CD ou développement local.

View File

@ -3,7 +3,18 @@ import dotenv from 'dotenv';
// Load environment variables from .env file // Load environment variables from .env file
dotenv.config(); dotenv.config();
export const config = { export interface AppConfig {
port: number;
apiKey: string;
databasePath: string;
relayUrls: string[];
autoRestart: boolean;
maxRestarts: number;
logLevel: string;
}
export function loadConfig(): AppConfig {
return {
port: parseInt(process.env.PORT || '9090'), port: parseInt(process.env.PORT || '9090'),
apiKey: process.env.API_KEY || 'your-api-key-change-this', apiKey: process.env.API_KEY || 'your-api-key-change-this',
databasePath: process.env.DATABASE_PATH || './data/server.db', databasePath: process.env.DATABASE_PATH || './data/server.db',
@ -11,7 +22,10 @@ export const config = {
autoRestart: process.env.AUTO_RESTART === 'true', autoRestart: process.env.AUTO_RESTART === 'true',
maxRestarts: parseInt(process.env.MAX_RESTARTS || '10'), maxRestarts: parseInt(process.env.MAX_RESTARTS || '10'),
logLevel: process.env.LOG_LEVEL || 'info' logLevel: process.env.LOG_LEVEL || 'info'
}; };
}
export const config: AppConfig = loadConfig();
// Validate required environment variables // Validate required environment variables
if (!config.apiKey || config.apiKey === 'your-api-key-change-this') { if (!config.apiKey || config.apiKey === 'your-api-key-change-this') {

View File

@ -40,7 +40,7 @@ export enum MessageType {
} }
// Re-export AnkFlag from WASM for relay message typing // Re-export AnkFlag from WASM for relay message typing
export { AnkFlag } from '../pkg/sdk_client'; export type { AnkFlag } from '../pkg/sdk_client';
// Message priority levels // Message priority levels
export enum MessagePriority { export enum MessagePriority {

View File

@ -1,5 +1,5 @@
import WebSocket from 'ws'; import WebSocket from 'ws';
import { AnkFlag } from '../pkg/sdk_client'; import type { AnkFlag } from '../pkg/sdk_client';
import { Service } from './service'; import { Service } from './service';
interface RelayConnection { interface RelayConnection {

View File

@ -11,7 +11,7 @@ const DEVICE_KEY = 'main_device';
export class Service { export class Service {
private static instance: Service; private static instance: Service;
private processes: Map<string, Process> = new Map(); private processes: Map<string, any> = new Map();
private membersList: any = {}; private membersList: any = {};
private relayManager: RelayManager; private relayManager: RelayManager;
private storages: string[] = []; // storage urls private storages: string[] = []; // storage urls
@ -103,7 +103,7 @@ export class Service {
if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data['pairedAddresses']) { if (lastCommitedState && lastCommitedState.public_data && lastCommitedState.public_data['pairedAddresses']) {
// This is a pairing process // This is a pairing process
try { try {
const pairedAddresses = this.decodeValue(lastCommitedState.public_data['pairedAddresses']); const pairedAddresses = this.decodeValue(lastCommitedState.public_data['pairedAddresses'] as unknown as number[]);
// Are we part of it? // Are we part of it?
if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) { if (pairedAddresses && pairedAddresses.length > 0 && pairedAddresses.includes(this.getDeviceAddress())) {
// We save the process to db // We save the process to db
@ -654,7 +654,7 @@ export class Service {
if (result.updated_process) { if (result.updated_process) {
// Update our cache // Update our cache
this.processes.set(process.states[0]?.state_id || 'unknown', result.updated_process.current_process); this.processes.set(result.updated_process.process_id, result.updated_process.current_process);
// Save to database // Save to database
await this.saveProcessToDb(result.updated_process.process_id, result.updated_process.current_process); await this.saveProcessToDb(result.updated_process.process_id, result.updated_process.current_process);
@ -757,7 +757,7 @@ export class Service {
// Update in-memory cache with all processes // Update in-memory cache with all processes
for (const [processId, process] of Object.entries(processes)) { for (const [processId, process] of Object.entries(processes)) {
this.processes.set(processId, process); this.processes.set(processId, process as any);
} }
return processes; return processes;
@ -878,7 +878,7 @@ export class Service {
if (process.states.length === 0) return null; if (process.states.length === 0) return null;
const processTip = process.states[process.states.length - 1].commited_in; const processTip = process.states[process.states.length - 1].commited_in;
for (let i = process.states.length - 1; i >= 0; i--) { for (let i = process.states.length - 1; i >= 0; i--) {
if (process.states[i].commited_in !== processTip) { if ((process.states[i] as any).commited_in !== processTip) {
return i; return i;
} }
} }

View File

@ -348,8 +348,8 @@ export class Server {
console.log('🔑 Not paired, creating pairing process...'); console.log('🔑 Not paired, creating pairing process...');
try { try {
const pairingResult = await service.createPairingProcess('', []); const pairingResult = await service.createPairingProcess('', []);
const processId: string = pairingResult.updated_process?.process_id; const processId = pairingResult.updated_process?.process_id as string;
const stateId = pairingResult.updated_process?.current_process?.states[0].state_id; const stateId = pairingResult.updated_process?.current_process?.states[0]?.state_id as string;
if (!processId || !stateId) { if (!processId || !stateId) {
throw new Error('Failed to get process id or state id'); throw new Error('Failed to get process id or state id');
} }

37
src/types/pkg__sdk_client.d.ts vendored Normal file
View File

@ -0,0 +1,37 @@
// Déclarations minimales pour le module WASM '../pkg/sdk_client'
// Ajuster si le package expose d'autres types/méthodes.
declare module '../pkg/sdk_client' {
export interface ProcessState {
state_id: string;
keys: Record<string, string>;
pcd_commitment: Record<string, string>;
public_data: Record<string, string>;
}
export interface Process {
process_id: string;
current_process: {
states: ProcessState[];
};
states: ProcessState[];
}
export interface ApiReturn {
updated_process?: Process;
}
export enum AnkFlag {}
export interface Device {}
export interface HandshakeMessage {}
export interface Member {}
export interface MerkleProofResult {}
export interface OutPointProcessMap {}
export interface RoleDefinition {}
export interface SecretsStore {}
export interface UserDiff {}
}

59
tests/config.test.ts Normal file
View File

@ -0,0 +1,59 @@
import { describe, it, expect, beforeEach } from 'vitest';
// Helper pour recharger le module avec de nouvelles variables d'env
async function loadConfig() {
const modulePath = '../src/config';
// Vitest supporte l'invalidation via import dynamique après resetModules
const mod = await import(modulePath);
return (mod.loadConfig as typeof import('../src/config').loadConfig)();
}
describe('config', () => {
const envBackup = { ...process.env };
beforeEach(async () => {
process.env = { ...envBackup };
delete process.env.PORT;
delete process.env.API_KEY;
delete process.env.DATABASE_PATH;
delete process.env.RELAY_URLS;
delete process.env.AUTO_RESTART;
delete process.env.MAX_RESTARTS;
delete process.env.LOG_LEVEL;
// @ts-ignore: vitest injecte resetModules via globalThis
if (typeof vi !== 'undefined') vi.resetModules();
});
it('charge les valeurs par défaut', async () => {
const cfg = await loadConfig();
expect(cfg.port).toBe(9090);
expect(cfg.apiKey).toBe('your-api-key-change-this');
expect(cfg.databasePath).toBe('./data/server.db');
expect(cfg.relayUrls).toEqual(['ws://localhost:8090']);
expect(cfg.autoRestart).toBe(false);
expect(cfg.maxRestarts).toBe(10);
expect(cfg.logLevel).toBe('info');
});
it('lit les variables denvironnement', async () => {
process.env.PORT = '1234';
process.env.API_KEY = 'k';
process.env.DATABASE_PATH = '/x.db';
process.env.RELAY_URLS = 'ws://a:1,ws://b:2';
process.env.AUTO_RESTART = 'true';
process.env.MAX_RESTARTS = '7';
process.env.LOG_LEVEL = 'debug';
// @ts-ignore
if (typeof vi !== 'undefined') vi.resetModules();
const cfg = await loadConfig();
expect(cfg.port).toBe(1234);
expect(cfg.apiKey).toBe('k');
expect(cfg.databasePath).toBe('/x.db');
expect(cfg.relayUrls).toEqual(['ws://a:1', 'ws://b:2']);
expect(cfg.autoRestart).toBe(true);
expect(cfg.maxRestarts).toBe(7);
expect(cfg.logLevel).toBe('debug');
});
});

21
tests/utils.test.ts Normal file
View File

@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { isValid32ByteHex, EMPTY32BYTES } from '../src/utils';
describe('utils', () => {
it('valide un hex 32 octets', () => {
const ok = 'a'.repeat(64);
expect(isValid32ByteHex(ok)).toBe(true);
});
it('rejette une longueur incorrecte', () => {
const bad = 'a'.repeat(63);
expect(isValid32ByteHex(bad)).toBe(false);
});
it('rejette des caractères non-hex', () => {
const bad = 'g'.repeat(64);
expect(isValid32ByteHex(bad)).toBe(false);
});
it('EMPTY32BYTES a bien 64 caractères hex', () => {
expect(EMPTY32BYTES.length).toBe(64);
expect(isValid32ByteHex(EMPTY32BYTES)).toBe(true);
});
});