organic-protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/PROTOCOL.md +131 -0
- package/README.md +30 -0
- package/dist/api.d.ts +103 -0
- package/dist/api.js +6 -0
- package/dist/identity.d.ts +30 -0
- package/dist/identity.js +38 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/qr.d.ts +77 -0
- package/dist/qr.js +100 -0
- package/dist/wire.d.ts +73 -0
- package/dist/wire.js +60 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 suipotryot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PROTOCOL.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Organic Money — Protocole inter-instances (v1)
|
|
2
|
+
|
|
3
|
+
Ce document est le **standard** que toute implémentation compatible (application ou serveur) doit respecter. Le paquet npm [`organic-protocol`](https://www.npmjs.com/package/organic-protocol) en est la traduction TypeScript exacte ; les implémentations JS/TS devraient l'importer plutôt que réimplémenter. La cryptographie (signatures, chaînes, chiffrement) est définie par [`organic-money`](https://www.npmjs.com/package/organic-money) et n'est pas répétée ici.
|
|
4
|
+
|
|
5
|
+
Version du protocole : **1**. Version du format QR : **1** (indépendante, voir §4).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Identité fédérée
|
|
10
|
+
|
|
11
|
+
Une clé publique seule ne suffit pas à joindre quelqu'un dans un réseau décentralisé. Une **identité** est toujours la paire :
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "pk": "<clé publique SECP256K1 compressée, hex>", "url": "<URL racine du serveur référent>" }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
L'`url` est l'URL **racine** du serveur (sans `/api`, sans slash final), normalisée : schéma `https` par défaut (`http` toléré s'il est explicite — réseaux locaux, développement), hôte en minuscules. Voir `normalizeServerUrl`.
|
|
18
|
+
|
|
19
|
+
## 2. Formats de fil (wire)
|
|
20
|
+
|
|
21
|
+
### 2.1 Transaction (`TxWire`)
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{ "v": 1, "d": 20260719, "t": 3, "p": "<pk cible>", "s": "<pk signataire>",
|
|
25
|
+
"m": [20260719001], "i": [], "h": "<signature DER hex>" }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Champ | Nom long | Contenu |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `v` | version | version du protocole (1) |
|
|
31
|
+
| `d` | date | entier `YYYYMMDD` |
|
|
32
|
+
| `t` | type | type de transaction (§3) |
|
|
33
|
+
| `p` | target | clé publique du destinataire |
|
|
34
|
+
| `s` | signer | clé publique de l'émetteur |
|
|
35
|
+
| `m` | money | ids d'unités de monnaie (`YYYYMMDDXXX`) |
|
|
36
|
+
| `i` | invests | ids d'unités d'invest (`YYYYMMDD9XXX`) |
|
|
37
|
+
| `h` | signature | signature SECP256K1 DER, hex |
|
|
38
|
+
|
|
39
|
+
### 2.2 Bloc (`BlockWire`)
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "v": 1, "d": 20260719, "p": "<signature du bloc précédent>", "s": "<pk>",
|
|
43
|
+
"m": [], "i": [], "t": 42, "r": "<racine de Merkle>", "h": "<signature>", "x": [ …TxWire ] }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`t` est ici le **total** (expérience économique cumulée) ; `p` est la signature du bloc précédent (chaînage) ; `x` la liste des transactions.
|
|
47
|
+
|
|
48
|
+
## 3. Types de transaction
|
|
49
|
+
|
|
50
|
+
| # | Type | # | Type |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| 1 | INIT | 8 | SETPAYER |
|
|
53
|
+
| 2 | CREATE | 9 | UNSETADMIN |
|
|
54
|
+
| 3 | PAY | 10 | UNSETACTOR |
|
|
55
|
+
| 4 | ENGAGE | 11 | UNSETPAYER |
|
|
56
|
+
| 5 | PAPER | 12 | PAYERORDER |
|
|
57
|
+
| 6 | SETADMIN | 13 | EARN |
|
|
58
|
+
| 7 | SETACTOR | | |
|
|
59
|
+
|
|
60
|
+
## 4. QR codes
|
|
61
|
+
|
|
62
|
+
Format : **`OM<version>:<TYPE>:<payload JSON>`** — le préfixe est hors JSON pour permettre le dispatch (et le rejet d'une version inconnue) sans parser. Version courante : `1`.
|
|
63
|
+
|
|
64
|
+
**Règle de compatibilité** : un lecteur qui rencontre une version qu'il ne connaît pas DOIT refuser proprement (« mettez à jour l'application ») et NE DOIT JAMAIS tenter d'interpréter au mieux.
|
|
65
|
+
|
|
66
|
+
| Type | Forme | Usage |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `CT` | `OM1:CT:{"pk","url","n","e"?}` | Carte de contact. `n` = nom affiché ; `e` = true si écosystème. |
|
|
69
|
+
| `TX` | `OM1:TX:{"tx":<TxWire PAY>,"url"}` | Paiement hors-ligne, caméra contre écran. `url` = serveur du PAYEUR, pour la vérification différée (§5, `tx/verify`). |
|
|
70
|
+
| `BR` | `OM1:BR:{"pk","url","n"}` | Demande de validation d'un nouveau citoyen. Les blocs sont récupérés via `GET {url}/api/v1/validations/{pk}` (Phase 2) — le QR est un pointeur, pas un conteneur. |
|
|
71
|
+
| `PP` | `OM1:PP:{"tx":<TxWire PAPER>}` | Billet papier imprimé. |
|
|
72
|
+
|
|
73
|
+
Contraintes : le `tx` d'un `TX` est de type PAY ; celui d'un `PP` est de type PAPER.
|
|
74
|
+
|
|
75
|
+
## 5. API REST `/api/v1`
|
|
76
|
+
|
|
77
|
+
Toutes les routes vivent sous `{url}/api/v1`. Corps en JSON. Les erreurs portent `{ "error": "<message>", "code": "<ApiErrorCode>"? }`.
|
|
78
|
+
|
|
79
|
+
### 5.1 Authentifications
|
|
80
|
+
|
|
81
|
+
**Block-auth** (`PUT /users/save`, `PUT /users/sign`) — en-tête `x-signature` = signature du hash du bloc soumis, par la clé du compte :
|
|
82
|
+
`x-signature = signHash(block.hash(), sk)`.
|
|
83
|
+
|
|
84
|
+
**Timestamp-auth** (`GET /tx/list`, `POST /users/password`, …) — en-tête `x-signature` = `signHash(sha256(publickey + ":" + timestamp), sk)` avec `publickey` et `timestamp` (secondes Unix) transmis en paramètres/corps. Tolérance : ±5 minutes, sinon 401.
|
|
85
|
+
|
|
86
|
+
### 5.2 Endpoints (Phase 1)
|
|
87
|
+
|
|
88
|
+
| Méthode & route | Auth | Corps → Réponse | Notes |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `GET /info` | — | → `InfoResponse` | carte d'identité publique du serveur |
|
|
91
|
+
| `GET /servers` | — | → `ServerListEntry[]` | annuaire des serveurs connus |
|
|
92
|
+
| `POST /users/register` | — | `RegisterBody` → `RegisterResponse` | la `secretkey` arrive CHIFFRÉE (opaque pour le serveur) ; le mot de passe est bcrypté à l'arrivée |
|
|
93
|
+
| `POST /users/login` | — | `LoginBody` → `LoginResponse` | **rotation du devicetoken** : l'appareil précédent est révoqué |
|
|
94
|
+
| `PUT /users/save` | block | `SaveBlockBody` → 200 | `409 DEVICE_REVOKED` si le devicetoken n'est plus l'actif |
|
|
95
|
+
| `PUT /users/sign` | block | `SignBlockBody` → 200 | le serveur signe le dernier bloc (billets, genèse) |
|
|
96
|
+
| `POST /users/password` | timestamp | `PasswordChangeBody` → 200 | met à jour bcrypt + sk re-chiffrée, sans rien lire |
|
|
97
|
+
| `POST /tx/send` | — | `TxSendBody` → 200 | **vérification croisée OBLIGATOIRE** (§5.3) |
|
|
98
|
+
| `GET /tx/list` | timestamp | → `TxWire[]` | paiements en attente pour la clé |
|
|
99
|
+
| `POST /tx/verify` | — | `TxVerifyBody` → `TxVerifyResponse` | lecture seule ; statuts : `confirmed` / `pending` / `invalid` / `unknown-sender` |
|
|
100
|
+
| `POST /papers/cash` | — | `PapersCashBody` → 200 | exige le PAPER complet, vérifie sa crypto ; `409 ALREADY_CASHED` |
|
|
101
|
+
| `GET /papers/isCashed?hash=` | — | → `IsCashedResponse` \| 404 | 404 = jamais encaissé |
|
|
102
|
+
|
|
103
|
+
### 5.3 Vérification croisée (obligation du serveur)
|
|
104
|
+
|
|
105
|
+
Avant d'accepter sur `tx/send` (et, en Phase 2, toute entrée d'écosystème), le serveur DOIT :
|
|
106
|
+
|
|
107
|
+
1. vérifier la validité cryptographique de la transaction ;
|
|
108
|
+
2. charger la chaîne **sauvegardée** de l'émetteur (`tx.s`) et la valider (`assertIsValid`) ;
|
|
109
|
+
3. vérifier que la transaction figure dans l'historique de cette chaîne (par sa signature `h`).
|
|
110
|
+
|
|
111
|
+
Sans l'étape 3, un attaquant peut signer une transaction portant des unités qu'il ne possède pas. Conséquence côté client : l'ordre **`pay → save → send` est strict**.
|
|
112
|
+
|
|
113
|
+
### 5.4 Un compte = un appareil actif
|
|
114
|
+
|
|
115
|
+
Le serveur émet un `devicetoken` opaque (UUID) au `register` et à chaque `login` ; seul le dernier émis est valide. Un `save` portant un jeton périmé reçoit `409 DEVICE_REVOKED` — l'application doit alors passer en lecture seule et proposer la restauration. Ce mécanisme n'est pas cryptographique (l'identité reste prouvée par `x-signature`) ; il empêche le fork accidentel d'une chaîne entre deux appareils.
|
|
116
|
+
|
|
117
|
+
### 5.5 Codes d'erreur
|
|
118
|
+
|
|
119
|
+
`INVALID_TX` · `INVALID_CHAIN` · `TX_NOT_IN_CHAIN` · `UNKNOWN_SENDER` · `UNKNOWN_USER` · `INVALID_SIGNATURE` · `DEVICE_REVOKED` · `ALREADY_CASHED`
|
|
120
|
+
|
|
121
|
+
## 6. Versionnement
|
|
122
|
+
|
|
123
|
+
- **Protocole** (`PROTOCOL_VERSION`, champ `v` des tx/blocs) : n'augmente que sur rupture du format de fil.
|
|
124
|
+
- **QR** (`QR_VERSION`, préfixe `OM<v>`) : n'augmente que sur rupture des payloads QR.
|
|
125
|
+
- **API** (`/api/v1`) : une rupture de contrat REST crée `/api/v2` ; `GET /info` annonce les versions parlées.
|
|
126
|
+
|
|
127
|
+
Les trois évoluent indépendamment. Toute implémentation DOIT annoncer ses versions via `GET /info` et refuser proprement ce qu'elle ne parle pas.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
*Licence MIT — © suipotryot. Les phases 2 (écosystèmes, validations) et 3 (fédération) étendront ce document sans casser la v1.*
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# organic-protocol
|
|
2
|
+
|
|
3
|
+
> The shared protocol of the [Organic Economy](https://economie-organique.fr) — wire formats, QR codes and REST contracts, verified by the TypeScript compiler.
|
|
4
|
+
|
|
5
|
+
This package carries the **shapes** that travel between Organic Economy instances: transaction and block wire formats, the versioned QR standard (`OM1:CT/TX/BR/PP`) with its encode/decode functions, and the DTOs of the `/api/v1` REST contract. It contains **no cryptography** — that lives in [`organic-money`](https://www.npmjs.com/package/organic-money).
|
|
6
|
+
|
|
7
|
+
The [organic-webapp](https://github.com/GuziEconomy/organic-webapp) client and the [organic-webserver](https://github.com/GuziEconomy/organic-webserver) server both import this package, so any drift between what one sends and the other expects becomes a compile error instead of a silent bug. Third-party implementations should either import it or implement [PROTOCOL.md](PROTOCOL.md), which is the same standard in prose.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install organic-protocol
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { decodeQr, encodeContactQr, UnsupportedQrVersionError } from 'organic-protocol'
|
|
15
|
+
|
|
16
|
+
const qr = encodeContactQr({ pk: '02ab…', url: 'https://trifouillis.fr', n: 'Alice' })
|
|
17
|
+
const decoded = decodeQr(qr) // { type: 'CT', payload: { pk, url, n } }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Development
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install
|
|
24
|
+
npm run build # tsc → dist/
|
|
25
|
+
npm test # mocha + chai (via tsx)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
[MIT](LICENSE) — © suipotryot
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTOs of the `/api/v1` REST contract — the exact request and response bodies
|
|
3
|
+
* every compatible server must speak. See PROTOCOL.md §5 for semantics
|
|
4
|
+
* (authentication, error cases, verification duties).
|
|
5
|
+
*/
|
|
6
|
+
import type { BlockWire, PublicKeyHex, TxWire } from './wire.js';
|
|
7
|
+
/** Machine-readable error codes carried alongside HTTP status codes. */
|
|
8
|
+
export type ApiErrorCode = 'INVALID_TX' | 'INVALID_CHAIN' | 'TX_NOT_IN_CHAIN' | 'UNKNOWN_SENDER' | 'UNKNOWN_USER' | 'INVALID_SIGNATURE' | 'DEVICE_REVOKED' | 'ALREADY_CASHED';
|
|
9
|
+
/** Error body returned with any non-2xx response. */
|
|
10
|
+
export interface ApiError {
|
|
11
|
+
error: string;
|
|
12
|
+
code?: ApiErrorCode;
|
|
13
|
+
}
|
|
14
|
+
export interface InfoResponse {
|
|
15
|
+
protocolVersion: number;
|
|
16
|
+
apiVersion: string;
|
|
17
|
+
/** human-readable server name, shown on the server-selection screen */
|
|
18
|
+
name: string;
|
|
19
|
+
serverPk: PublicKeyHex;
|
|
20
|
+
/** public key of the core ecosystem of this server (null until Phase 2) */
|
|
21
|
+
corePk: PublicKeyHex | null;
|
|
22
|
+
stats: {
|
|
23
|
+
users: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** One entry of the known-servers directory. */
|
|
27
|
+
export interface ServerListEntry {
|
|
28
|
+
name: string;
|
|
29
|
+
url: string;
|
|
30
|
+
}
|
|
31
|
+
export type ServersResponse = ServerListEntry[];
|
|
32
|
+
export interface RegisterBody {
|
|
33
|
+
publickey: PublicKeyHex;
|
|
34
|
+
name: string;
|
|
35
|
+
mail: string;
|
|
36
|
+
/** LOGIN password (bcrypted server-side). Never the encryption password. */
|
|
37
|
+
password: string;
|
|
38
|
+
/** ISO date of birth, e.g. "1990-03-15" */
|
|
39
|
+
birthdate: string;
|
|
40
|
+
/** the secret key AES-encrypted client-side — opaque to the server */
|
|
41
|
+
secretkey: string;
|
|
42
|
+
/** the chain holding exactly one BirthBlock awaiting validation */
|
|
43
|
+
blocks: BlockWire[];
|
|
44
|
+
}
|
|
45
|
+
export interface RegisterResponse {
|
|
46
|
+
publickey: PublicKeyHex;
|
|
47
|
+
/** the validated chain (server-signed in genesis mode) */
|
|
48
|
+
blocks: BlockWire[];
|
|
49
|
+
devicetoken: string;
|
|
50
|
+
}
|
|
51
|
+
export interface LoginBody {
|
|
52
|
+
mail: string;
|
|
53
|
+
password: string;
|
|
54
|
+
}
|
|
55
|
+
/** A successful login rotates the devicetoken: the previous device is revoked. */
|
|
56
|
+
export interface LoginResponse {
|
|
57
|
+
publickey: PublicKeyHex;
|
|
58
|
+
name: string;
|
|
59
|
+
mail: string;
|
|
60
|
+
/** the AES-encrypted secret key, exactly as uploaded */
|
|
61
|
+
secretkey: string;
|
|
62
|
+
blocks: BlockWire[];
|
|
63
|
+
devicetoken: string;
|
|
64
|
+
}
|
|
65
|
+
export interface SaveBlockBody {
|
|
66
|
+
publickey: PublicKeyHex;
|
|
67
|
+
block: BlockWire;
|
|
68
|
+
devicetoken: string;
|
|
69
|
+
}
|
|
70
|
+
export interface SignBlockBody {
|
|
71
|
+
publickey: PublicKeyHex;
|
|
72
|
+
block: BlockWire;
|
|
73
|
+
}
|
|
74
|
+
export interface PasswordChangeBody {
|
|
75
|
+
publickey: PublicKeyHex;
|
|
76
|
+
/** Unix timestamp (seconds) also used by the timestamp-auth signature */
|
|
77
|
+
timestamp: number;
|
|
78
|
+
newpassword: string;
|
|
79
|
+
/** the secret key re-encrypted client-side with the new password */
|
|
80
|
+
secretkey: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The server MUST cross-verify before queueing: load the sender's saved chain,
|
|
84
|
+
* assert it is valid, and check the transaction exists in its history.
|
|
85
|
+
*/
|
|
86
|
+
export interface TxSendBody {
|
|
87
|
+
tx: TxWire;
|
|
88
|
+
}
|
|
89
|
+
export type TxListResponse = TxWire[];
|
|
90
|
+
export interface TxVerifyBody {
|
|
91
|
+
tx: TxWire;
|
|
92
|
+
}
|
|
93
|
+
export type TxVerifyStatus = 'confirmed' | 'pending' | 'invalid' | 'unknown-sender';
|
|
94
|
+
export interface TxVerifyResponse {
|
|
95
|
+
status: TxVerifyStatus;
|
|
96
|
+
}
|
|
97
|
+
/** The full PAPER transaction is required — the server verifies it before registering the hash. */
|
|
98
|
+
export interface PapersCashBody {
|
|
99
|
+
tx: TxWire;
|
|
100
|
+
}
|
|
101
|
+
export interface IsCashedResponse {
|
|
102
|
+
id: number | string;
|
|
103
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federated identity: in a decentralized network, a public key alone is not
|
|
3
|
+
* enough to reach someone — you also need to know which server hosts them.
|
|
4
|
+
* An identity is therefore always the pair { pk, url }.
|
|
5
|
+
*/
|
|
6
|
+
import type { PublicKeyHex } from './wire.js';
|
|
7
|
+
/** A reachable actor of the network: a key and its referent server. */
|
|
8
|
+
export interface Identity {
|
|
9
|
+
pk: PublicKeyHex;
|
|
10
|
+
/** Root URL of the referent server, normalized (no trailing slash, no /api). */
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
export type ContactType = 'citizen' | 'ecosystem';
|
|
14
|
+
/** A saved contact, as stored by the app. */
|
|
15
|
+
export interface Contact extends Identity {
|
|
16
|
+
name: string;
|
|
17
|
+
type: ContactType;
|
|
18
|
+
}
|
|
19
|
+
export declare class InvalidServerUrlError extends Error {
|
|
20
|
+
constructor(raw: string);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a user-supplied server URL to its canonical form:
|
|
24
|
+
* - adds `https://` when no scheme is given (plain `http:` is kept if explicit,
|
|
25
|
+
* for local networks and development)
|
|
26
|
+
* - lowercases the host
|
|
27
|
+
* - strips trailing slashes and a trailing `/api` (identities carry the ROOT url)
|
|
28
|
+
* @throws InvalidServerUrlError when the input cannot be parsed as an URL.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeServerUrl(raw: string): string;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federated identity: in a decentralized network, a public key alone is not
|
|
3
|
+
* enough to reach someone — you also need to know which server hosts them.
|
|
4
|
+
* An identity is therefore always the pair { pk, url }.
|
|
5
|
+
*/
|
|
6
|
+
export class InvalidServerUrlError extends Error {
|
|
7
|
+
constructor(raw) {
|
|
8
|
+
super(`Invalid server URL: "${raw}"`);
|
|
9
|
+
this.name = 'InvalidServerUrlError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a user-supplied server URL to its canonical form:
|
|
14
|
+
* - adds `https://` when no scheme is given (plain `http:` is kept if explicit,
|
|
15
|
+
* for local networks and development)
|
|
16
|
+
* - lowercases the host
|
|
17
|
+
* - strips trailing slashes and a trailing `/api` (identities carry the ROOT url)
|
|
18
|
+
* @throws InvalidServerUrlError when the input cannot be parsed as an URL.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeServerUrl(raw) {
|
|
21
|
+
const trimmed = raw.trim();
|
|
22
|
+
if (trimmed === '')
|
|
23
|
+
throw new InvalidServerUrlError(raw);
|
|
24
|
+
const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
25
|
+
let url;
|
|
26
|
+
try {
|
|
27
|
+
url = new URL(withScheme);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new InvalidServerUrlError(raw);
|
|
31
|
+
}
|
|
32
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
33
|
+
throw new InvalidServerUrlError(raw);
|
|
34
|
+
let path = url.pathname.replace(/\/+$/, '');
|
|
35
|
+
if (path.toLowerCase().endsWith('/api'))
|
|
36
|
+
path = path.slice(0, -4);
|
|
37
|
+
return `${url.protocol}//${url.host}${path}`;
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PROTOCOL_VERSION, TxType, isTxWire, isBlockWire, type IntDate, type UnitId, type PublicKeyHex, type SignatureHex, type TxWire, type BlockWire, } from './wire.js';
|
|
2
|
+
export { InvalidServerUrlError, normalizeServerUrl, type Identity, type Contact, type ContactType, } from './identity.js';
|
|
3
|
+
export { QR_VERSION, QrError, UnsupportedQrVersionError, InvalidQrError, encodeContactQr, encodeOfflineTxQr, encodeValidationQr, encodePaperQr, decodeQr, type QrType, type DecodedQr, type ContactQrPayload, type OfflineTxQrPayload, type ValidationQrPayload, type PaperQrPayload, } from './qr.js';
|
|
4
|
+
export type { ApiError, ApiErrorCode, InfoResponse, ServerListEntry, ServersResponse, RegisterBody, RegisterResponse, LoginBody, LoginResponse, SaveBlockBody, SignBlockBody, PasswordChangeBody, TxSendBody, TxListResponse, TxVerifyBody, TxVerifyStatus, TxVerifyResponse, PapersCashBody, IsCashedResponse, } from './api.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { PROTOCOL_VERSION, TxType, isTxWire, isBlockWire, } from './wire.js';
|
|
2
|
+
export { InvalidServerUrlError, normalizeServerUrl, } from './identity.js';
|
|
3
|
+
export { QR_VERSION, QrError, UnsupportedQrVersionError, InvalidQrError, encodeContactQr, encodeOfflineTxQr, encodeValidationQr, encodePaperQr, decodeQr, } from './qr.js';
|
package/dist/qr.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR code formats — the inter-instance standard for camera-to-screen exchange.
|
|
3
|
+
*
|
|
4
|
+
* Every Organic Money QR is the string `OM<version>:<TYPE>:<JSON payload>`.
|
|
5
|
+
* The prefix lives OUTSIDE the JSON so any reader can dispatch (or reject an
|
|
6
|
+
* unknown version) without parsing. Current version: 1.
|
|
7
|
+
*
|
|
8
|
+
* Types:
|
|
9
|
+
* CT — contact card {pk, url, n, e?}
|
|
10
|
+
* TX — offline payment {tx, url}
|
|
11
|
+
* BR — validation request {pk, url, n}
|
|
12
|
+
* PP — paper money {tx}
|
|
13
|
+
*/
|
|
14
|
+
import { type PublicKeyHex, type TxWire } from './wire.js';
|
|
15
|
+
/** Current QR format version (independent of PROTOCOL_VERSION on purpose). */
|
|
16
|
+
export declare const QR_VERSION = 1;
|
|
17
|
+
export type QrType = 'CT' | 'TX' | 'BR' | 'PP';
|
|
18
|
+
/** Contact card: shown by `account-details`, scanned by `add-contact`. */
|
|
19
|
+
export interface ContactQrPayload {
|
|
20
|
+
pk: PublicKeyHex;
|
|
21
|
+
url: string;
|
|
22
|
+
/** display name */
|
|
23
|
+
n: string;
|
|
24
|
+
/** true when the contact is an ecosystem */
|
|
25
|
+
e?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Offline payment: a signed PAY transaction handed camera-to-screen. */
|
|
28
|
+
export interface OfflineTxQrPayload {
|
|
29
|
+
tx: TxWire;
|
|
30
|
+
/** root URL of the PAYER's server, for deferred verification (tx/verify) */
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
/** Validation request: shown by a candidate citizen, scanned by a core-ecosystem admin. */
|
|
34
|
+
export interface ValidationQrPayload {
|
|
35
|
+
pk: PublicKeyHex;
|
|
36
|
+
url: string;
|
|
37
|
+
n: string;
|
|
38
|
+
}
|
|
39
|
+
/** Paper money: a signed PAPER transaction printed on a bill. */
|
|
40
|
+
export interface PaperQrPayload {
|
|
41
|
+
tx: TxWire;
|
|
42
|
+
}
|
|
43
|
+
export type DecodedQr = {
|
|
44
|
+
type: 'CT';
|
|
45
|
+
payload: ContactQrPayload;
|
|
46
|
+
} | {
|
|
47
|
+
type: 'TX';
|
|
48
|
+
payload: OfflineTxQrPayload;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'BR';
|
|
51
|
+
payload: ValidationQrPayload;
|
|
52
|
+
} | {
|
|
53
|
+
type: 'PP';
|
|
54
|
+
payload: PaperQrPayload;
|
|
55
|
+
};
|
|
56
|
+
export declare class QrError extends Error {
|
|
57
|
+
}
|
|
58
|
+
/** The scanned QR is an Organic Money QR of a version this build doesn't speak. */
|
|
59
|
+
export declare class UnsupportedQrVersionError extends QrError {
|
|
60
|
+
readonly version: number;
|
|
61
|
+
constructor(version: number);
|
|
62
|
+
}
|
|
63
|
+
/** The scanned text is not a valid Organic Money QR. */
|
|
64
|
+
export declare class InvalidQrError extends QrError {
|
|
65
|
+
constructor(reason: string);
|
|
66
|
+
}
|
|
67
|
+
export declare const encodeContactQr: (payload: ContactQrPayload) => string;
|
|
68
|
+
export declare const encodeOfflineTxQr: (payload: OfflineTxQrPayload) => string;
|
|
69
|
+
export declare const encodeValidationQr: (payload: ValidationQrPayload) => string;
|
|
70
|
+
export declare const encodePaperQr: (payload: PaperQrPayload) => string;
|
|
71
|
+
/**
|
|
72
|
+
* Decode any Organic Money QR.
|
|
73
|
+
* @throws UnsupportedQrVersionError for a QR of a newer/unknown version —
|
|
74
|
+
* the app must tell the user to update, never guess.
|
|
75
|
+
* @throws InvalidQrError for anything that is not a well-formed OM QR.
|
|
76
|
+
*/
|
|
77
|
+
export declare function decodeQr(text: string): DecodedQr;
|
package/dist/qr.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR code formats — the inter-instance standard for camera-to-screen exchange.
|
|
3
|
+
*
|
|
4
|
+
* Every Organic Money QR is the string `OM<version>:<TYPE>:<JSON payload>`.
|
|
5
|
+
* The prefix lives OUTSIDE the JSON so any reader can dispatch (or reject an
|
|
6
|
+
* unknown version) without parsing. Current version: 1.
|
|
7
|
+
*
|
|
8
|
+
* Types:
|
|
9
|
+
* CT — contact card {pk, url, n, e?}
|
|
10
|
+
* TX — offline payment {tx, url}
|
|
11
|
+
* BR — validation request {pk, url, n}
|
|
12
|
+
* PP — paper money {tx}
|
|
13
|
+
*/
|
|
14
|
+
import { isTxWire, TxType } from './wire.js';
|
|
15
|
+
/** Current QR format version (independent of PROTOCOL_VERSION on purpose). */
|
|
16
|
+
export const QR_VERSION = 1;
|
|
17
|
+
export class QrError extends Error {
|
|
18
|
+
}
|
|
19
|
+
/** The scanned QR is an Organic Money QR of a version this build doesn't speak. */
|
|
20
|
+
export class UnsupportedQrVersionError extends QrError {
|
|
21
|
+
version;
|
|
22
|
+
constructor(version) {
|
|
23
|
+
super(`Unsupported Organic Money QR version ${version} (this app speaks version ${QR_VERSION}). Please update the app.`);
|
|
24
|
+
this.version = version;
|
|
25
|
+
this.name = 'UnsupportedQrVersionError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** The scanned text is not a valid Organic Money QR. */
|
|
29
|
+
export class InvalidQrError extends QrError {
|
|
30
|
+
constructor(reason) {
|
|
31
|
+
super(`Invalid Organic Money QR: ${reason}`);
|
|
32
|
+
this.name = 'InvalidQrError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const PREFIX_RE = /^OM(\d+):([A-Z]{2}):([\s\S]*)$/;
|
|
36
|
+
const encode = (type, payload) => `OM${QR_VERSION}:${type}:${JSON.stringify(payload)}`;
|
|
37
|
+
export const encodeContactQr = (payload) => encode('CT', payload);
|
|
38
|
+
export const encodeOfflineTxQr = (payload) => encode('TX', payload);
|
|
39
|
+
export const encodeValidationQr = (payload) => encode('BR', payload);
|
|
40
|
+
export const encodePaperQr = (payload) => encode('PP', payload);
|
|
41
|
+
const isNonEmptyString = (s) => typeof s === 'string' && s.length > 0;
|
|
42
|
+
function assertContact(p) {
|
|
43
|
+
if (!isNonEmptyString(p.pk) || !isNonEmptyString(p.url) || !isNonEmptyString(p.n))
|
|
44
|
+
throw new InvalidQrError('contact payload requires pk, url and n');
|
|
45
|
+
if (p.e !== undefined && typeof p.e !== 'boolean')
|
|
46
|
+
throw new InvalidQrError('contact field e must be a boolean');
|
|
47
|
+
}
|
|
48
|
+
function assertValidation(p) {
|
|
49
|
+
if (!isNonEmptyString(p.pk) || !isNonEmptyString(p.url) || !isNonEmptyString(p.n))
|
|
50
|
+
throw new InvalidQrError('validation payload requires pk, url and n');
|
|
51
|
+
}
|
|
52
|
+
function assertTx(p, requireUrl, expectedType) {
|
|
53
|
+
if (!isTxWire(p.tx))
|
|
54
|
+
throw new InvalidQrError('payload requires a well-formed tx');
|
|
55
|
+
if (requireUrl && !isNonEmptyString(p.url))
|
|
56
|
+
throw new InvalidQrError('payload requires the payer server url');
|
|
57
|
+
if (expectedType !== undefined && p.tx.t !== expectedType)
|
|
58
|
+
throw new InvalidQrError(`tx must be of type ${TxType[expectedType]}`);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Decode any Organic Money QR.
|
|
62
|
+
* @throws UnsupportedQrVersionError for a QR of a newer/unknown version —
|
|
63
|
+
* the app must tell the user to update, never guess.
|
|
64
|
+
* @throws InvalidQrError for anything that is not a well-formed OM QR.
|
|
65
|
+
*/
|
|
66
|
+
export function decodeQr(text) {
|
|
67
|
+
const match = PREFIX_RE.exec(text.trim());
|
|
68
|
+
if (!match)
|
|
69
|
+
throw new InvalidQrError('not an OM<v>:<TYPE>:<payload> string');
|
|
70
|
+
const version = Number(match[1]);
|
|
71
|
+
if (version !== QR_VERSION)
|
|
72
|
+
throw new UnsupportedQrVersionError(version);
|
|
73
|
+
const type = match[2];
|
|
74
|
+
let payload;
|
|
75
|
+
try {
|
|
76
|
+
payload = JSON.parse(match[3]);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new InvalidQrError('payload is not valid JSON');
|
|
80
|
+
}
|
|
81
|
+
if (typeof payload !== 'object' || payload === null)
|
|
82
|
+
throw new InvalidQrError('payload must be a JSON object');
|
|
83
|
+
const p = payload;
|
|
84
|
+
switch (type) {
|
|
85
|
+
case 'CT':
|
|
86
|
+
assertContact(p);
|
|
87
|
+
return { type: 'CT', payload: { pk: p.pk, url: p.url, n: p.n, ...(p.e !== undefined ? { e: p.e } : {}) } };
|
|
88
|
+
case 'TX':
|
|
89
|
+
assertTx(p, true, TxType.PAY);
|
|
90
|
+
return { type: 'TX', payload: { tx: p.tx, url: p.url } };
|
|
91
|
+
case 'BR':
|
|
92
|
+
assertValidation(p);
|
|
93
|
+
return { type: 'BR', payload: { pk: p.pk, url: p.url, n: p.n } };
|
|
94
|
+
case 'PP':
|
|
95
|
+
assertTx(p, false, TxType.PAPER);
|
|
96
|
+
return { type: 'PP', payload: { tx: p.tx } };
|
|
97
|
+
default:
|
|
98
|
+
throw new InvalidQrError(`unknown QR type "${type}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire formats of the Organic Money protocol — the exact shapes that travel
|
|
3
|
+
* between instances (app ↔ server, QR codes, blockchain exports).
|
|
4
|
+
* Mirrors the serialization implemented by organic-money.js.
|
|
5
|
+
*/
|
|
6
|
+
/** Current protocol version. Bump only on breaking wire-format changes. */
|
|
7
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
8
|
+
/** Transaction types (see PROTOCOL.md §3). */
|
|
9
|
+
export declare enum TxType {
|
|
10
|
+
INIT = 1,
|
|
11
|
+
CREATE = 2,
|
|
12
|
+
PAY = 3,
|
|
13
|
+
ENGAGE = 4,
|
|
14
|
+
PAPER = 5,
|
|
15
|
+
SETADMIN = 6,
|
|
16
|
+
SETACTOR = 7,
|
|
17
|
+
SETPAYER = 8,
|
|
18
|
+
UNSETADMIN = 9,
|
|
19
|
+
UNSETACTOR = 10,
|
|
20
|
+
UNSETPAYER = 11,
|
|
21
|
+
PAYERORDER = 12,
|
|
22
|
+
EARN = 13
|
|
23
|
+
}
|
|
24
|
+
/** Date as a YYYYMMDD integer, e.g. 20260719. */
|
|
25
|
+
export type IntDate = number;
|
|
26
|
+
/**
|
|
27
|
+
* Money unit id (`YYYYMMDDXXX`, e.g. 20260719003) or invest unit id
|
|
28
|
+
* (`YYYYMMDD9XXX`, e.g. 202607199003 — the `9` separator marks invests).
|
|
29
|
+
* The creation date is embedded in the id.
|
|
30
|
+
*/
|
|
31
|
+
export type UnitId = number;
|
|
32
|
+
/** Compressed SECP256K1 public key, hex-encoded (33 bytes → 66 chars). */
|
|
33
|
+
export type PublicKeyHex = string;
|
|
34
|
+
/** DER-encoded SECP256K1 signature, hex-encoded. */
|
|
35
|
+
export type SignatureHex = string;
|
|
36
|
+
/**
|
|
37
|
+
* A transaction in wire format (short field names for compactness).
|
|
38
|
+
* Field meanings: v=version, d=date, t=type, p=target, s=signer,
|
|
39
|
+
* m=money ids, i=invest ids, h=signature.
|
|
40
|
+
*/
|
|
41
|
+
export interface TxWire {
|
|
42
|
+
v: number;
|
|
43
|
+
d: IntDate;
|
|
44
|
+
t: TxType;
|
|
45
|
+
p: PublicKeyHex;
|
|
46
|
+
s: PublicKeyHex;
|
|
47
|
+
m: UnitId[];
|
|
48
|
+
i: UnitId[];
|
|
49
|
+
h: SignatureHex;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A sealed (or open) block in wire format.
|
|
53
|
+
* Field meanings: v=version, d=closedate, p=previous block signature,
|
|
54
|
+
* s=signer, m=available money at seal time, i=available invests,
|
|
55
|
+
* t=total (economic experience), r=merkle root, h=block signature,
|
|
56
|
+
* x=transactions.
|
|
57
|
+
*/
|
|
58
|
+
export interface BlockWire {
|
|
59
|
+
v: number;
|
|
60
|
+
d: IntDate;
|
|
61
|
+
p: string;
|
|
62
|
+
s: PublicKeyHex;
|
|
63
|
+
m: UnitId[];
|
|
64
|
+
i: UnitId[];
|
|
65
|
+
t: number;
|
|
66
|
+
r: string;
|
|
67
|
+
h: SignatureHex;
|
|
68
|
+
x: TxWire[];
|
|
69
|
+
}
|
|
70
|
+
/** Structural check that an unknown value is a well-formed TxWire. */
|
|
71
|
+
export declare function isTxWire(o: unknown): o is TxWire;
|
|
72
|
+
/** Structural check that an unknown value is a well-formed BlockWire. */
|
|
73
|
+
export declare function isBlockWire(o: unknown): o is BlockWire;
|
package/dist/wire.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire formats of the Organic Money protocol — the exact shapes that travel
|
|
3
|
+
* between instances (app ↔ server, QR codes, blockchain exports).
|
|
4
|
+
* Mirrors the serialization implemented by organic-money.js.
|
|
5
|
+
*/
|
|
6
|
+
/** Current protocol version. Bump only on breaking wire-format changes. */
|
|
7
|
+
export const PROTOCOL_VERSION = 1;
|
|
8
|
+
/** Transaction types (see PROTOCOL.md §3). */
|
|
9
|
+
export var TxType;
|
|
10
|
+
(function (TxType) {
|
|
11
|
+
TxType[TxType["INIT"] = 1] = "INIT";
|
|
12
|
+
TxType[TxType["CREATE"] = 2] = "CREATE";
|
|
13
|
+
TxType[TxType["PAY"] = 3] = "PAY";
|
|
14
|
+
TxType[TxType["ENGAGE"] = 4] = "ENGAGE";
|
|
15
|
+
TxType[TxType["PAPER"] = 5] = "PAPER";
|
|
16
|
+
TxType[TxType["SETADMIN"] = 6] = "SETADMIN";
|
|
17
|
+
TxType[TxType["SETACTOR"] = 7] = "SETACTOR";
|
|
18
|
+
TxType[TxType["SETPAYER"] = 8] = "SETPAYER";
|
|
19
|
+
TxType[TxType["UNSETADMIN"] = 9] = "UNSETADMIN";
|
|
20
|
+
TxType[TxType["UNSETACTOR"] = 10] = "UNSETACTOR";
|
|
21
|
+
TxType[TxType["UNSETPAYER"] = 11] = "UNSETPAYER";
|
|
22
|
+
TxType[TxType["PAYERORDER"] = 12] = "PAYERORDER";
|
|
23
|
+
TxType[TxType["EARN"] = 13] = "EARN";
|
|
24
|
+
})(TxType || (TxType = {}));
|
|
25
|
+
const isHex = (s) => typeof s === 'string' && /^[0-9a-fA-F]*$/.test(s);
|
|
26
|
+
const isUnitIdArray = (a) => Array.isArray(a) && a.every((n) => typeof n === 'number' && Number.isInteger(n));
|
|
27
|
+
/** Structural check that an unknown value is a well-formed TxWire. */
|
|
28
|
+
export function isTxWire(o) {
|
|
29
|
+
if (typeof o !== 'object' || o === null)
|
|
30
|
+
return false;
|
|
31
|
+
const t = o;
|
|
32
|
+
return (typeof t.v === 'number' &&
|
|
33
|
+
typeof t.d === 'number' &&
|
|
34
|
+
Number.isInteger(t.d) &&
|
|
35
|
+
typeof t.t === 'number' &&
|
|
36
|
+
t.t >= TxType.INIT &&
|
|
37
|
+
t.t <= TxType.EARN &&
|
|
38
|
+
isHex(t.p) &&
|
|
39
|
+
isHex(t.s) &&
|
|
40
|
+
isUnitIdArray(t.m) &&
|
|
41
|
+
isUnitIdArray(t.i) &&
|
|
42
|
+
isHex(t.h));
|
|
43
|
+
}
|
|
44
|
+
/** Structural check that an unknown value is a well-formed BlockWire. */
|
|
45
|
+
export function isBlockWire(o) {
|
|
46
|
+
if (typeof o !== 'object' || o === null)
|
|
47
|
+
return false;
|
|
48
|
+
const b = o;
|
|
49
|
+
return (typeof b.v === 'number' &&
|
|
50
|
+
typeof b.d === 'number' &&
|
|
51
|
+
isHex(b.p) &&
|
|
52
|
+
isHex(b.s) &&
|
|
53
|
+
isUnitIdArray(b.m) &&
|
|
54
|
+
isUnitIdArray(b.i) &&
|
|
55
|
+
typeof b.t === 'number' &&
|
|
56
|
+
isHex(b.r) &&
|
|
57
|
+
isHex(b.h) &&
|
|
58
|
+
Array.isArray(b.x) &&
|
|
59
|
+
b.x.every(isTxWire));
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "organic-protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The shared protocol of the Organic Economy: wire formats, QR codes and API contracts, verified by the TypeScript compiler. Companion of organic-money.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"organic-economy",
|
|
7
|
+
"organic-money",
|
|
8
|
+
"protocol",
|
|
9
|
+
"qr"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "suipotryot",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"PROTOCOL.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test": "mocha",
|
|
29
|
+
"prepublishOnly": "npm run build && npm test"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/chai": "^5.0.1",
|
|
33
|
+
"@types/mocha": "^10.0.10",
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
35
|
+
"chai": "^5.1.2",
|
|
36
|
+
"mocha": "^11.0.1",
|
|
37
|
+
"tsx": "^4.19.2",
|
|
38
|
+
"typescript": "^5.7.2"
|
|
39
|
+
}
|
|
40
|
+
}
|