organic-protocol 0.2.0 → 0.3.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/PROTOCOL.md +168 -135
- package/dist/api.d.ts +122 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/wire.d.ts +34 -4
- package/dist/wire.js +46 -5
- package/package.json +48 -48
package/PROTOCOL.md
CHANGED
|
@@ -1,135 +1,168 @@
|
|
|
1
|
-
# Organic Money — Inter-instance Protocol (v1)
|
|
2
|
-
|
|
3
|
-
This document is the **standard** every compatible implementation (app or server) must follow. The [`organic-protocol`](https://www.npmjs.com/package/organic-protocol) npm package is its exact TypeScript translation; JS/TS implementations should import it rather than reimplement. Cryptography (signatures, chains, encryption) is defined by [`organic-money`](https://www.npmjs.com/package/organic-money) and is not repeated here.
|
|
4
|
-
|
|
5
|
-
Protocol version: **1**. QR format version: **1** (independent, see §4).
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. Federated identity
|
|
10
|
-
|
|
11
|
-
A public key alone is not enough to reach someone in a decentralized network. An **identity** is always the pair:
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
{ "pk": "<compressed SECP256K1 public key, hex>", "url": "<root URL of the referent server>" }
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
The `url` is the server's **root** URL (no `/api`, no trailing slash), normalized: `https` scheme by default (`http` tolerated when explicit — local networks, development), lowercase host. See `normalizeServerUrl`.
|
|
18
|
-
|
|
19
|
-
## 2. Wire formats
|
|
20
|
-
|
|
21
|
-
### 2.1 Transaction (`TxWire`)
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{ "v": 1, "d": 20260719, "t": 3, "p": "<target pk>", "s": "<signer pk>",
|
|
25
|
-
"m": "BLeiCZk=", "i": "", "h": "<DER signature, hex>" }
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
| Field | Long name | Content |
|
|
29
|
-
|---|---|---|
|
|
30
|
-
| `v` | version | protocol version (1) |
|
|
31
|
-
| `d` | date | `YYYYMMDD` integer |
|
|
32
|
-
| `t` | type | transaction type (§3) |
|
|
33
|
-
| `p` | target | recipient's public key |
|
|
34
|
-
| `s` | signer | sender's public key |
|
|
35
|
-
| `m` | money | packed money unit ids (§2.3) |
|
|
36
|
-
| `i` | invests | packed invest unit ids (§2.3) |
|
|
37
|
-
| `h` | signature | SECP256K1 DER signature, hex |
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
### 5.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
1
|
+
# Organic Money — Inter-instance Protocol (v1)
|
|
2
|
+
|
|
3
|
+
This document is the **standard** every compatible implementation (app or server) must follow. The [`organic-protocol`](https://www.npmjs.com/package/organic-protocol) npm package is its exact TypeScript translation; JS/TS implementations should import it rather than reimplement. Cryptography (signatures, chains, encryption) is defined by [`organic-money`](https://www.npmjs.com/package/organic-money) and is not repeated here.
|
|
4
|
+
|
|
5
|
+
Protocol version: **1**. QR format version: **1** (independent, see §4).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Federated identity
|
|
10
|
+
|
|
11
|
+
A public key alone is not enough to reach someone in a decentralized network. An **identity** is always the pair:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "pk": "<compressed SECP256K1 public key, hex>", "url": "<root URL of the referent server>" }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The `url` is the server's **root** URL (no `/api`, no trailing slash), normalized: `https` scheme by default (`http` tolerated when explicit — local networks, development), lowercase host. See `normalizeServerUrl`.
|
|
18
|
+
|
|
19
|
+
## 2. Wire formats
|
|
20
|
+
|
|
21
|
+
### 2.1 Transaction (`TxWire`)
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{ "v": 1, "d": 20260719, "t": 3, "p": "<target pk>", "s": "<signer pk>",
|
|
25
|
+
"m": "BLeiCZk=", "i": "", "h": "<DER signature, hex>" }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Field | Long name | Content |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `v` | version | protocol version (1) |
|
|
31
|
+
| `d` | date | `YYYYMMDD` integer |
|
|
32
|
+
| `t` | type | transaction type (§3) |
|
|
33
|
+
| `p` | target | recipient's public key |
|
|
34
|
+
| `s` | signer | sender's public key |
|
|
35
|
+
| `m` | money | packed money unit ids (§2.3) |
|
|
36
|
+
| `i` | invests | packed invest unit ids (§2.3) |
|
|
37
|
+
| `h` | signature | SECP256K1 DER signature, hex |
|
|
38
|
+
| `q`? | quantity | ratio (SETACTOR) or spending cap, `-1` = unlimited (SETPAYER) — required on those two types only |
|
|
39
|
+
| `e`? | ecosystem | target ecosystem's public key — required on SETADMIN/SETACTOR/SETPAYER/UNSETADMIN/UNSETACTOR/UNSETPAYER/PAYERORDER, absent on every other type |
|
|
40
|
+
| `x`? | exercised order | EARN only: signature of the PayerOrder it fulfills, present only when produced by `order()` |
|
|
41
|
+
|
|
42
|
+
`q`/`e`/`x` are part of the signed preimage on the types where they apply — not cosmetic metadata. They must be absent on every type that doesn't use them (§3 lists which).
|
|
43
|
+
|
|
44
|
+
### 2.2 Block (`BlockWire`)
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{ "v": 1, "d": 20260719, "p": "<previous block signature>", "s": "<pk>",
|
|
48
|
+
"m": "", "i": "", "t": 1, "e": 42, "r": "<merkle root>", "h": "<signature>", "x": [ …TxWire ] }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Here `t` is the **block type** (not a total, despite the letter — see the table below); `p` is the previous block's signature (chain link); `x` is the transaction list. `m`/`i` are the available (unspent) money/invest ids at seal time, packed the same way as in a transaction (§2.3). `e` is the cumulative economic experience — present only on citizen block types, absent on ecosystem ones.
|
|
52
|
+
|
|
53
|
+
| `t` | Block type | Carries `e`? |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| 1 | CITIZEN | yes |
|
|
56
|
+
| 2 | ECOSYSTEM | no |
|
|
57
|
+
| 3 | CITIZENBIRTH | yes |
|
|
58
|
+
| 4 | CITIZENINIT | yes |
|
|
59
|
+
| 5 | ECOSYSTEMBIRTH | no |
|
|
60
|
+
| 6 | ECOSYSTEMINIT | no |
|
|
61
|
+
|
|
62
|
+
### 2.3 Packed unit ids (`m`, `i`)
|
|
63
|
+
|
|
64
|
+
Money and invest unit ids (`YYYYMMDDXXX` / `YYYYMMDD9XXX`, §2.1 table) are **not** a JSON array of numbers on the wire — a plain decimal array costs 12 bytes per id (11-12 digits plus a separator) for ~5 bytes of actual information, which matters once a paper bill (§4, `PP`) bundles many ids into one QR code. Instead: each id is packed into 5 big-endian bytes (comfortably covers every possible id), all ids are concatenated, and the result is base64-encoded into a single string. An empty array is the empty string `""`. Order and duplicates are preserved. Reference implementation: `packUnitIds`/`unpackUnitIds` in [`organic-money`](https://www.npmjs.com/package/organic-money) (`src/crypto.js`); `organic-protocol` validates the shape (`isTxWire`/`isBlockWire`) but does not implement the packing itself.
|
|
65
|
+
|
|
66
|
+
## 3. Transaction types
|
|
67
|
+
|
|
68
|
+
| # | Type | # | Type |
|
|
69
|
+
|---|---|---|---|
|
|
70
|
+
| 1 | INIT | 8 | SETPAYER |
|
|
71
|
+
| 2 | CREATE | 9 | UNSETADMIN |
|
|
72
|
+
| 3 | PAY | 10 | UNSETACTOR |
|
|
73
|
+
| 4 | ENGAGE | 11 | UNSETPAYER |
|
|
74
|
+
| 5 | PAPER | 12 | PAYERORDER |
|
|
75
|
+
| 6 | SETADMIN | 13 | EARN |
|
|
76
|
+
| 7 | SETACTOR | | |
|
|
77
|
+
|
|
78
|
+
## 4. QR codes
|
|
79
|
+
|
|
80
|
+
Format: **`OM<version>:<TYPE>:<JSON payload>`** — the prefix lives outside the JSON so any reader can dispatch (or reject an unknown version) without parsing. Current version: `1`.
|
|
81
|
+
|
|
82
|
+
**Compatibility rule**: a reader encountering a version it does not speak MUST refuse cleanly ("please update the app") and MUST NEVER attempt a best-effort interpretation.
|
|
83
|
+
|
|
84
|
+
| Type | Shape | Usage |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `CT` | `OM1:CT:{"pk","url","n","e"?}` | Contact card. `n` = display name; `e` = true for an ecosystem. |
|
|
87
|
+
| `TX` | `OM1:TX:{"tx":<PAY TxWire>,"url"}` | Offline payment, camera-to-screen. `url` = the PAYER's server, for deferred verification (§5, `tx/verify`). |
|
|
88
|
+
| `BR` | `OM1:BR:{"pk","url","n"}` | Validation request of a new citizen. The scanning admin's own device fetches the candidate's blocks via `GET {url}/api/v1/validations/{pk}` (§5.2b) — the QR is a pointer, not a container. |
|
|
89
|
+
| `PP` | `OM1:PP:{"tx":<PAPER TxWire>}` | Printed paper bill. |
|
|
90
|
+
|
|
91
|
+
Constraints: the `tx` of a `TX` is of type PAY; the one of a `PP` is of type PAPER.
|
|
92
|
+
|
|
93
|
+
## 5. REST API `/api/v1`
|
|
94
|
+
|
|
95
|
+
All routes live under `{url}/api/v1`. JSON bodies. Errors carry `{ "error": "<message>", "code": "<ApiErrorCode>"? }`.
|
|
96
|
+
|
|
97
|
+
### 5.1 Authentication schemes
|
|
98
|
+
|
|
99
|
+
**Block-auth** (`PUT /users/save`, `PUT /users/sign`) — `x-signature` header = signature of the submitted block's hash, by the account key:
|
|
100
|
+
`x-signature = signHash(block.hash(), sk)`.
|
|
101
|
+
|
|
102
|
+
**Timestamp-auth** (`GET /tx/list`, `POST /users/password`, …) — `x-signature` header = `signHash(sha256(publickey + ":" + timestamp), sk)` with `publickey` and `timestamp` (Unix seconds) passed as params/body. Tolerance: ±5 minutes, otherwise 401.
|
|
103
|
+
|
|
104
|
+
### 5.2 Endpoints (Phase 1)
|
|
105
|
+
|
|
106
|
+
| Method & route | Auth | Body → Response | Notes |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `GET /info` | — | → `InfoResponse` | public identity card of the server |
|
|
109
|
+
| `GET /servers` | — | → `ServerListEntry[]` | directory of known servers |
|
|
110
|
+
| `POST /users/register` | — | `RegisterBody` → `RegisterResponse` | the `secretkey` arrives ENCRYPTED (opaque to the server); the password is bcrypted on arrival |
|
|
111
|
+
| `POST /users/login` | — | `LoginBody` → `LoginResponse` | **devicetoken rotation**: the previous device is revoked |
|
|
112
|
+
| `PUT /users/save` | block | `SaveBlockBody` → 200 | `409 DEVICE_REVOKED` when the devicetoken is no longer the active one |
|
|
113
|
+
| `PUT /users/sign` | block | `SignBlockBody` → 200 | the server signs the last block (bills, genesis) |
|
|
114
|
+
| `POST /users/password` | timestamp | `PasswordChangeBody` → 200 | updates bcrypt + re-encrypted sk, without reading anything |
|
|
115
|
+
| `POST /tx/send` | — | `TxSendBody` → 200 | **MANDATORY cross-verification** (§5.3) |
|
|
116
|
+
| `GET /tx/list` | timestamp | → `TxWire[]` | pending payments for the key |
|
|
117
|
+
| `POST /tx/verify` | — | `TxVerifyBody` → `TxVerifyResponse` | read-only; statuses: `confirmed` / `pending` / `invalid` / `unknown-sender` |
|
|
118
|
+
| `POST /papers/cash` | — | `PapersCashBody` → 200 | requires the full PAPER, verifies its crypto; `409 ALREADY_CASHED` |
|
|
119
|
+
| `GET /papers/isCashed?hash=` | — | → `IsCashedResponse` \| 404 | 404 = never cashed |
|
|
120
|
+
|
|
121
|
+
### 5.2b Endpoints (Phase 2 — ecosystems & validations)
|
|
122
|
+
|
|
123
|
+
Ecosystem creation is free and instant — no admin approval, see the notes column. A citizen's validation is the only thing gated behind the core ecosystem's admins.
|
|
124
|
+
|
|
125
|
+
| Method & route | Auth | Body → Response | Notes |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `POST /ecosystems` | timestamp | `EcosystemCreateBody` → `EcosystemCreateResponse` | server generates the ecosystem's own key, self-signs and self-validates it; `founderPk` is recorded as `validatorpk` for attribution only. First ecosystem ever created on a server becomes its core (`iscore: true`) |
|
|
128
|
+
| `GET /ecosystems?lat&lng&radiusKm` | — | → `EcosystemListEntry[]` | public directory; sorted by distance when `lat`/`lng` given |
|
|
129
|
+
| `GET /ecosystems/mine?publickey=` | — | → `MyEcosystemEntry[]` | public — roles are already derivable from each ecosystem's own public chain, this just aggregates across all of them server-side |
|
|
130
|
+
| `GET /ecosystems/:pk` | — | → `EcosystemInfoResponse` | full chain + metadata |
|
|
131
|
+
| `PUT /ecosystems/:pk/meta` | timestamp | `EcosystemMetaUpdateBody` → 200 | caller must be `isAdmin(publickey)` on that ecosystem |
|
|
132
|
+
| `POST /ecosystems/:pk/tx` | — | `EcosystemTxBody` → 200 | generic ingress for PAY/ENGAGE/role/PAYERORDER transactions targeting this ecosystem — same cross-verification duty as `tx/send` (§5.3), against the *signer's* own chain. A `PAYERORDER` executes immediately; the resulting payment is queued for a citizen target or applied directly for an ecosystem target — no separate claim step |
|
|
133
|
+
| `POST /ecosystems/:pk/distribute` | timestamp | `EcosystemDistributeBody` → 200 | caller must be `isAdmin`/`isPayer`; manual only, no scheduling |
|
|
134
|
+
| `GET /validations` | timestamp | → `ValidationListEntry[]` | caller must be a current admin of the server's core ecosystem |
|
|
135
|
+
| `GET /validations/:pk` | timestamp | → `ValidationDetailResponse` | same auth; the admin's device reconstructs the chain locally and calls `validateAccount()` itself |
|
|
136
|
+
| `GET /validations/status/:pk` | — | → `ValidationStatusResponse` | public — for the candidate to poll |
|
|
137
|
+
| `POST /validations/:pk/approve` | block | `ValidationApproveBody` → 200 | `block` is the admin's own already-signed `InitializationBlock`; the server's real check is that its signer is *currently* a core admin (`403 NOT_CORE_ADMIN` otherwise), not just that the signature is valid |
|
|
138
|
+
| `POST /validations/:pk/reject` | timestamp | `ValidationRejectBody` → 200 | caller must be a core admin; the row is kept with `status: 'rejected'`, not deleted |
|
|
139
|
+
|
|
140
|
+
### 5.3 Cross-verification (server duty)
|
|
141
|
+
|
|
142
|
+
Before accepting on `tx/send` **or `ecosystems/:pk/tx`**, the server MUST:
|
|
143
|
+
|
|
144
|
+
1. verify the cryptographic validity of the transaction;
|
|
145
|
+
2. load the SAVED chain of the sender (`tx.s`) and validate it (`assertIsValid`) **and confirm it is validated (`isValidated`)** — a pending account's self-signed chain is well-formed but not yet trustworthy;
|
|
146
|
+
3. verify the transaction exists in that chain's history (by its signature `h`).
|
|
147
|
+
|
|
148
|
+
Without step 3, an attacker can sign a transaction carrying units they do not own. Without step 2's validation check, an unapproved candidate could act as if already validated. Client-side consequence: the **`pay → save → send` order is strict**.
|
|
149
|
+
|
|
150
|
+
### 5.4 One account = one active device
|
|
151
|
+
|
|
152
|
+
The server issues an opaque `devicetoken` (UUID) at `register` and at every `login`; only the last one issued is valid. A `save` carrying a stale token receives `409 DEVICE_REVOKED` — the app must then switch to read-only and offer the restore flow. This mechanism is not cryptographic (identity is still proven by `x-signature`); it prevents the accidental fork of a chain across two devices.
|
|
153
|
+
|
|
154
|
+
### 5.5 Error codes
|
|
155
|
+
|
|
156
|
+
`INVALID_TX` · `INVALID_CHAIN` · `TX_NOT_IN_CHAIN` · `UNKNOWN_SENDER` · `UNKNOWN_USER` · `INVALID_SIGNATURE` · `DEVICE_REVOKED` · `ALREADY_CASHED` · `NOT_CORE_ADMIN` · `ALREADY_VALIDATED`
|
|
157
|
+
|
|
158
|
+
## 6. Versioning
|
|
159
|
+
|
|
160
|
+
- **Protocol** (`PROTOCOL_VERSION`, `v` field of txs/blocks): bumped only on a wire-format break.
|
|
161
|
+
- **QR** (`QR_VERSION`, `OM<v>` prefix): bumped only on a QR payload break.
|
|
162
|
+
- **API** (`/api/v1`): a REST contract break creates `/api/v2`; `GET /info` announces the spoken versions.
|
|
163
|
+
|
|
164
|
+
The three evolve independently. Every implementation MUST announce its versions via `GET /info` and refuse cleanly what it does not speak.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
*MIT license — © suipotryot. Phase 2 (ecosystems, validations) is now specified above (§5.2b) without breaking v1. Phase 3 (federation) will extend this document further.*
|
package/dist/api.d.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { BlockWire, PublicKeyHex, TxWire } from './wire.js';
|
|
7
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'
|
|
8
|
+
export type ApiErrorCode = 'INVALID_TX' | 'INVALID_CHAIN' | 'TX_NOT_IN_CHAIN' | 'UNKNOWN_SENDER' | 'UNKNOWN_USER' | 'INVALID_SIGNATURE' | 'DEVICE_REVOKED' | 'ALREADY_CASHED'
|
|
9
|
+
/** the signer proved their identity but isn't currently a core admin */
|
|
10
|
+
| 'NOT_CORE_ADMIN'
|
|
11
|
+
/** approve attempted on an account that is already active */
|
|
12
|
+
| 'ALREADY_VALIDATED';
|
|
13
|
+
/** A citizen or ecosystem account's standing (Phase 2). */
|
|
14
|
+
export type MembershipStatus = 'pending-validation' | 'active' | 'rejected';
|
|
9
15
|
/** Error body returned with any non-2xx response. */
|
|
10
16
|
export interface ApiError {
|
|
11
17
|
error: string;
|
|
@@ -44,7 +50,9 @@ export interface RegisterBody {
|
|
|
44
50
|
}
|
|
45
51
|
export interface RegisterResponse {
|
|
46
52
|
publickey: PublicKeyHex;
|
|
47
|
-
/** the
|
|
53
|
+
/** 'active' only for the server's very first account (open genesis); 'pending-validation' otherwise */
|
|
54
|
+
status: MembershipStatus;
|
|
55
|
+
/** the birth-only chain if pending, or the validated chain if this was the open-genesis account */
|
|
48
56
|
blocks: BlockWire[];
|
|
49
57
|
devicetoken: string;
|
|
50
58
|
}
|
|
@@ -59,6 +67,7 @@ export interface LoginResponse {
|
|
|
59
67
|
mail: string;
|
|
60
68
|
/** the AES-encrypted secret key, exactly as uploaded */
|
|
61
69
|
secretkey: string;
|
|
70
|
+
status: MembershipStatus;
|
|
62
71
|
blocks: BlockWire[];
|
|
63
72
|
devicetoken: string;
|
|
64
73
|
}
|
|
@@ -101,3 +110,114 @@ export interface PapersCashBody {
|
|
|
101
110
|
export interface IsCashedResponse {
|
|
102
111
|
id: number | string;
|
|
103
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Creation is free and instant (§0.3 of Phase-2.md): the server generates the
|
|
115
|
+
* ecosystem's own key, self-signs its birth block, and self-validates with
|
|
116
|
+
* that same key — no separate approval step. founderPk is only the
|
|
117
|
+
* authenticated requester, recorded as validatorpk for attribution; it is
|
|
118
|
+
* not the signer of the resulting chain.
|
|
119
|
+
*/
|
|
120
|
+
export interface EcosystemCreateBody {
|
|
121
|
+
founderPk: PublicKeyHex;
|
|
122
|
+
timestamp: number;
|
|
123
|
+
name: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
lat?: number;
|
|
126
|
+
lng?: number;
|
|
127
|
+
}
|
|
128
|
+
export interface EcosystemCreateResponse {
|
|
129
|
+
publickey: PublicKeyHex;
|
|
130
|
+
blocks: BlockWire[];
|
|
131
|
+
/** true only for the very first ecosystem ever created on this server */
|
|
132
|
+
iscore: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** Directory entry — metadata lives server-side, not on-chain. */
|
|
135
|
+
export interface EcosystemListEntry {
|
|
136
|
+
publickey: PublicKeyHex;
|
|
137
|
+
name: string;
|
|
138
|
+
description: string | null;
|
|
139
|
+
lat: number | null;
|
|
140
|
+
lng: number | null;
|
|
141
|
+
iscore: boolean;
|
|
142
|
+
/** present only when the request carried lat/lng */
|
|
143
|
+
distanceKm?: number;
|
|
144
|
+
}
|
|
145
|
+
export type EcosystemListResponse = EcosystemListEntry[];
|
|
146
|
+
/**
|
|
147
|
+
* Roles are re-carried onto every new block (see organic-money's
|
|
148
|
+
* EcosystemBlockchain), so the server only ever needs to look at each
|
|
149
|
+
* ecosystem's current last block to answer this — no full-chain replay.
|
|
150
|
+
* Public: ecosystem membership is already derivable by anyone from each
|
|
151
|
+
* ecosystem's own (public) chain — this just saves the client from having
|
|
152
|
+
* to query every ecosystem on the server one by one to find out.
|
|
153
|
+
*/
|
|
154
|
+
export interface MyEcosystemEntry {
|
|
155
|
+
publickey: PublicKeyHex;
|
|
156
|
+
name: string;
|
|
157
|
+
role: 'admin' | 'actor' | 'payer';
|
|
158
|
+
}
|
|
159
|
+
export type MyEcosystemsResponse = MyEcosystemEntry[];
|
|
160
|
+
export interface EcosystemInfoResponse {
|
|
161
|
+
publickey: PublicKeyHex;
|
|
162
|
+
name: string;
|
|
163
|
+
description: string | null;
|
|
164
|
+
lat: number | null;
|
|
165
|
+
lng: number | null;
|
|
166
|
+
iscore: boolean;
|
|
167
|
+
blocks: BlockWire[];
|
|
168
|
+
}
|
|
169
|
+
export interface EcosystemMetaUpdateBody {
|
|
170
|
+
publickey: PublicKeyHex;
|
|
171
|
+
timestamp: number;
|
|
172
|
+
name?: string;
|
|
173
|
+
description?: string;
|
|
174
|
+
lat?: number;
|
|
175
|
+
lng?: number;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Generic ingress for any citizen-signed transaction targeting this
|
|
179
|
+
* ecosystem: PAY, ENGAGE, the role transactions (SETADMIN/SETACTOR/
|
|
180
|
+
* SETPAYER/UNSET*), and PAYERORDER. Same cross-verification duty as
|
|
181
|
+
* POST /tx/send (PROTOCOL.md §5.3), against the *signer's* own chain.
|
|
182
|
+
* A PAYERORDER is executed immediately: the resulting payment is routed
|
|
183
|
+
* exactly like a normal payment (queued for a citizen target, applied
|
|
184
|
+
* directly for an ecosystem target on this same server) — there is no
|
|
185
|
+
* separate "claim" step.
|
|
186
|
+
*/
|
|
187
|
+
export interface EcosystemTxBody {
|
|
188
|
+
tx: TxWire;
|
|
189
|
+
}
|
|
190
|
+
/** Manual only (Phase 2) — no scheduled/automatic distribution. */
|
|
191
|
+
export interface EcosystemDistributeBody {
|
|
192
|
+
publickey: PublicKeyHex;
|
|
193
|
+
timestamp: number;
|
|
194
|
+
}
|
|
195
|
+
export interface ValidationListEntry {
|
|
196
|
+
pk: PublicKeyHex;
|
|
197
|
+
name: string;
|
|
198
|
+
requestedAt: string;
|
|
199
|
+
}
|
|
200
|
+
export type ValidationListResponse = ValidationListEntry[];
|
|
201
|
+
/** The candidate's not-yet-validated chain, for the admin's device to locally call validateAccount(). */
|
|
202
|
+
export interface ValidationDetailResponse {
|
|
203
|
+
name: string;
|
|
204
|
+
blocks: BlockWire[];
|
|
205
|
+
}
|
|
206
|
+
export interface ValidationStatusResponse {
|
|
207
|
+
status: MembershipStatus;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* block is the InitializationBlock the admin already signed locally with
|
|
211
|
+
* their own key (requireBlockAuth already proves that signature). The
|
|
212
|
+
* server's own job is the part signature-proof alone can't give it: check
|
|
213
|
+
* the signer is *currently* one of the core ecosystem's admins.
|
|
214
|
+
*/
|
|
215
|
+
export interface ValidationApproveBody {
|
|
216
|
+
publickey: PublicKeyHex;
|
|
217
|
+
block: BlockWire;
|
|
218
|
+
}
|
|
219
|
+
export interface ValidationRejectBody {
|
|
220
|
+
publickey: PublicKeyHex;
|
|
221
|
+
timestamp: number;
|
|
222
|
+
reason?: string;
|
|
223
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { PROTOCOL_VERSION, TxType, isTxWire, isBlockWire, type IntDate, type UnitId, type PackedUnitIds, type PublicKeyHex, type SignatureHex, type TxWire, type BlockWire, } from './wire.js';
|
|
1
|
+
export { PROTOCOL_VERSION, TxType, BlockType, isTxWire, isBlockWire, type IntDate, type UnitId, type PackedUnitIds, type PublicKeyHex, type SignatureHex, type TxWire, type BlockWire, } from './wire.js';
|
|
2
2
|
export { InvalidServerUrlError, normalizeServerUrl, type Identity, type Contact, type ContactType, } from './identity.js';
|
|
3
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';
|
|
4
|
+
export type { ApiError, ApiErrorCode, MembershipStatus, InfoResponse, ServerListEntry, ServersResponse, RegisterBody, RegisterResponse, LoginBody, LoginResponse, SaveBlockBody, SignBlockBody, PasswordChangeBody, TxSendBody, TxListResponse, TxVerifyBody, TxVerifyStatus, TxVerifyResponse, PapersCashBody, IsCashedResponse, EcosystemCreateBody, EcosystemCreateResponse, EcosystemListEntry, EcosystemListResponse, MyEcosystemEntry, MyEcosystemsResponse, EcosystemInfoResponse, EcosystemMetaUpdateBody, EcosystemTxBody, EcosystemDistributeBody, ValidationListEntry, ValidationListResponse, ValidationDetailResponse, ValidationStatusResponse, ValidationApproveBody, ValidationRejectBody, } from './api.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { PROTOCOL_VERSION, TxType, isTxWire, isBlockWire, } from './wire.js';
|
|
1
|
+
export { PROTOCOL_VERSION, TxType, BlockType, isTxWire, isBlockWire, } from './wire.js';
|
|
2
2
|
export { InvalidServerUrlError, normalizeServerUrl, } from './identity.js';
|
|
3
3
|
export { QR_VERSION, QrError, UnsupportedQrVersionError, InvalidQrError, encodeContactQr, encodeOfflineTxQr, encodeValidationQr, encodePaperQr, decodeQr, } from './qr.js';
|
package/dist/wire.d.ts
CHANGED
|
@@ -21,6 +21,20 @@ export declare enum TxType {
|
|
|
21
21
|
PAYERORDER = 12,
|
|
22
22
|
EARN = 13
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Block types (see PROTOCOL.md §2.2). Mirrors organic-money's BLOCKTYPE —
|
|
26
|
+
* this is the block's own `t` field, not to be confused with TxType above.
|
|
27
|
+
* CITIZEN/CITIZENBIRTH/CITIZENINIT carry economic experience (`e` on
|
|
28
|
+
* BlockWire); the ECOSYSTEM* kinds don't.
|
|
29
|
+
*/
|
|
30
|
+
export declare enum BlockType {
|
|
31
|
+
CITIZEN = 1,
|
|
32
|
+
ECOSYSTEM = 2,
|
|
33
|
+
CITIZENBIRTH = 3,
|
|
34
|
+
CITIZENINIT = 4,
|
|
35
|
+
ECOSYSTEMBIRTH = 5,
|
|
36
|
+
ECOSYSTEMINIT = 6
|
|
37
|
+
}
|
|
24
38
|
/** Date as a YYYYMMDD integer, e.g. 20260719. */
|
|
25
39
|
export type IntDate = number;
|
|
26
40
|
/**
|
|
@@ -47,6 +61,14 @@ export type SignatureHex = string;
|
|
|
47
61
|
* A transaction in wire format (short field names for compactness).
|
|
48
62
|
* Field meanings: v=version, d=date, t=type, p=target, s=signer,
|
|
49
63
|
* m=money ids, i=invest ids, h=signature.
|
|
64
|
+
*
|
|
65
|
+
* Ecosystem-related types (see PROTOCOL.md §3) carry extra fields, all part
|
|
66
|
+
* of the signed preimage (not cosmetic metadata):
|
|
67
|
+
* q — ratio (SETACTOR) or spending cap, -1 = unlimited (SETPAYER)
|
|
68
|
+
* e — target ecosystem's public key (SETADMIN/SETACTOR/SETPAYER/
|
|
69
|
+
* UNSETADMIN/UNSETACTOR/UNSETPAYER/PAYERORDER)
|
|
70
|
+
* x — EARN only: signature of the PayerOrder it fulfills, present only
|
|
71
|
+
* when produced by order() (absent for distributeSalary()/earn())
|
|
50
72
|
*/
|
|
51
73
|
export interface TxWire {
|
|
52
74
|
v: number;
|
|
@@ -57,13 +79,20 @@ export interface TxWire {
|
|
|
57
79
|
m: PackedUnitIds;
|
|
58
80
|
i: PackedUnitIds;
|
|
59
81
|
h: SignatureHex;
|
|
82
|
+
q?: number;
|
|
83
|
+
e?: PublicKeyHex;
|
|
84
|
+
x?: SignatureHex;
|
|
60
85
|
}
|
|
61
86
|
/**
|
|
62
87
|
* A sealed (or open) block in wire format.
|
|
63
88
|
* Field meanings: v=version, d=closedate, p=previous block signature,
|
|
64
89
|
* s=signer, m=available money at seal time, i=available invests,
|
|
65
|
-
* t=
|
|
66
|
-
* x=transactions.
|
|
90
|
+
* t=block type (see BlockType — NOT a total, despite the letter), r=merkle
|
|
91
|
+
* root, h=block signature, x=transactions.
|
|
92
|
+
*
|
|
93
|
+
* e = cumulative economic experience, present only when `t` is a citizen
|
|
94
|
+
* block type (CITIZEN/CITIZENBIRTH/CITIZENINIT) — ecosystem blocks have no
|
|
95
|
+
* experience at all.
|
|
67
96
|
*/
|
|
68
97
|
export interface BlockWire {
|
|
69
98
|
v: number;
|
|
@@ -72,12 +101,13 @@ export interface BlockWire {
|
|
|
72
101
|
s: PublicKeyHex;
|
|
73
102
|
m: PackedUnitIds;
|
|
74
103
|
i: PackedUnitIds;
|
|
75
|
-
t:
|
|
104
|
+
t: BlockType;
|
|
76
105
|
r: string;
|
|
77
106
|
h: SignatureHex;
|
|
78
107
|
x: TxWire[];
|
|
108
|
+
e?: number;
|
|
79
109
|
}
|
|
80
|
-
/** Structural check that an unknown value is a well-formed TxWire. */
|
|
110
|
+
/** Structural check that an unknown value is a well-formed TxWire, including its type-specific fields. */
|
|
81
111
|
export declare function isTxWire(o: unknown): o is TxWire;
|
|
82
112
|
/** Structural check that an unknown value is a well-formed BlockWire. */
|
|
83
113
|
export declare function isBlockWire(o: unknown): o is BlockWire;
|
package/dist/wire.js
CHANGED
|
@@ -22,14 +22,35 @@ export var TxType;
|
|
|
22
22
|
TxType[TxType["PAYERORDER"] = 12] = "PAYERORDER";
|
|
23
23
|
TxType[TxType["EARN"] = 13] = "EARN";
|
|
24
24
|
})(TxType || (TxType = {}));
|
|
25
|
+
/**
|
|
26
|
+
* Block types (see PROTOCOL.md §2.2). Mirrors organic-money's BLOCKTYPE —
|
|
27
|
+
* this is the block's own `t` field, not to be confused with TxType above.
|
|
28
|
+
* CITIZEN/CITIZENBIRTH/CITIZENINIT carry economic experience (`e` on
|
|
29
|
+
* BlockWire); the ECOSYSTEM* kinds don't.
|
|
30
|
+
*/
|
|
31
|
+
export var BlockType;
|
|
32
|
+
(function (BlockType) {
|
|
33
|
+
BlockType[BlockType["CITIZEN"] = 1] = "CITIZEN";
|
|
34
|
+
BlockType[BlockType["ECOSYSTEM"] = 2] = "ECOSYSTEM";
|
|
35
|
+
BlockType[BlockType["CITIZENBIRTH"] = 3] = "CITIZENBIRTH";
|
|
36
|
+
BlockType[BlockType["CITIZENINIT"] = 4] = "CITIZENINIT";
|
|
37
|
+
BlockType[BlockType["ECOSYSTEMBIRTH"] = 5] = "ECOSYSTEMBIRTH";
|
|
38
|
+
BlockType[BlockType["ECOSYSTEMINIT"] = 6] = "ECOSYSTEMINIT";
|
|
39
|
+
})(BlockType || (BlockType = {}));
|
|
25
40
|
const isHex = (s) => typeof s === 'string' && /^[0-9a-fA-F]*$/.test(s);
|
|
26
41
|
const isPackedUnitIds = (s) => typeof s === 'string' && /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s);
|
|
27
|
-
/**
|
|
42
|
+
/** Types requiring both q (ratio/cap) and e (target ecosystem). */
|
|
43
|
+
const TX_TYPES_WITH_Q_AND_E = [TxType.SETACTOR, TxType.SETPAYER];
|
|
44
|
+
/** Types requiring e (target ecosystem) alone. */
|
|
45
|
+
const TX_TYPES_WITH_E_ONLY = [
|
|
46
|
+
TxType.SETADMIN, TxType.UNSETADMIN, TxType.UNSETACTOR, TxType.UNSETPAYER, TxType.PAYERORDER,
|
|
47
|
+
];
|
|
48
|
+
/** Structural check that an unknown value is a well-formed TxWire, including its type-specific fields. */
|
|
28
49
|
export function isTxWire(o) {
|
|
29
50
|
if (typeof o !== 'object' || o === null)
|
|
30
51
|
return false;
|
|
31
52
|
const t = o;
|
|
32
|
-
|
|
53
|
+
if (!(typeof t.v === 'number' &&
|
|
33
54
|
typeof t.d === 'number' &&
|
|
34
55
|
Number.isInteger(t.d) &&
|
|
35
56
|
typeof t.t === 'number' &&
|
|
@@ -39,22 +60,42 @@ export function isTxWire(o) {
|
|
|
39
60
|
isHex(t.s) &&
|
|
40
61
|
isPackedUnitIds(t.m) &&
|
|
41
62
|
isPackedUnitIds(t.i) &&
|
|
42
|
-
isHex(t.h))
|
|
63
|
+
isHex(t.h)))
|
|
64
|
+
return false;
|
|
65
|
+
const type = t.t;
|
|
66
|
+
if (TX_TYPES_WITH_Q_AND_E.includes(type)) {
|
|
67
|
+
return typeof t.q === 'number' && isHex(t.e) && t.x === undefined;
|
|
68
|
+
}
|
|
69
|
+
if (TX_TYPES_WITH_E_ONLY.includes(type)) {
|
|
70
|
+
return t.q === undefined && isHex(t.e) && t.x === undefined;
|
|
71
|
+
}
|
|
72
|
+
if (type === TxType.EARN) {
|
|
73
|
+
return t.q === undefined && t.e === undefined && (t.x === undefined || isHex(t.x));
|
|
74
|
+
}
|
|
75
|
+
// INIT/CREATE/PAY/ENGAGE/PAPER: none of q/e/x apply.
|
|
76
|
+
return t.q === undefined && t.e === undefined && t.x === undefined;
|
|
43
77
|
}
|
|
78
|
+
/** Block types carrying economic experience (`e`) — citizen blocks only. */
|
|
79
|
+
const BLOCK_TYPES_WITH_EXPERIENCE = [BlockType.CITIZEN, BlockType.CITIZENBIRTH, BlockType.CITIZENINIT];
|
|
44
80
|
/** Structural check that an unknown value is a well-formed BlockWire. */
|
|
45
81
|
export function isBlockWire(o) {
|
|
46
82
|
if (typeof o !== 'object' || o === null)
|
|
47
83
|
return false;
|
|
48
84
|
const b = o;
|
|
49
|
-
|
|
85
|
+
if (!(typeof b.v === 'number' &&
|
|
50
86
|
typeof b.d === 'number' &&
|
|
51
87
|
isHex(b.p) &&
|
|
52
88
|
isHex(b.s) &&
|
|
53
89
|
isPackedUnitIds(b.m) &&
|
|
54
90
|
isPackedUnitIds(b.i) &&
|
|
55
91
|
typeof b.t === 'number' &&
|
|
92
|
+
b.t >= BlockType.CITIZEN &&
|
|
93
|
+
b.t <= BlockType.ECOSYSTEMINIT &&
|
|
56
94
|
isHex(b.r) &&
|
|
57
95
|
isHex(b.h) &&
|
|
58
96
|
Array.isArray(b.x) &&
|
|
59
|
-
b.x.every(isTxWire))
|
|
97
|
+
b.x.every(isTxWire)))
|
|
98
|
+
return false;
|
|
99
|
+
const hasExperience = BLOCK_TYPES_WITH_EXPERIENCE.includes(b.t);
|
|
100
|
+
return hasExperience ? typeof b.e === 'number' : b.e === undefined;
|
|
60
101
|
}
|
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "organic-protocol",
|
|
3
|
-
"version": "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
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/OrganicEconomy/organic-protocol.git"
|
|
16
|
-
},
|
|
17
|
-
"bugs": {
|
|
18
|
-
"url": "https://github.com/OrganicEconomy/organic-protocol/issues"
|
|
19
|
-
},
|
|
20
|
-
"homepage": "https://github.com/OrganicEconomy/organic-protocol#readme",
|
|
21
|
-
"type": "module",
|
|
22
|
-
"main": "./dist/index.js",
|
|
23
|
-
"types": "./dist/index.d.ts",
|
|
24
|
-
"exports": {
|
|
25
|
-
".": {
|
|
26
|
-
"types": "./dist/index.d.ts",
|
|
27
|
-
"import": "./dist/index.js"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"files": [
|
|
31
|
-
"dist",
|
|
32
|
-
"PROTOCOL.md"
|
|
33
|
-
],
|
|
34
|
-
"scripts": {
|
|
35
|
-
"build": "tsc",
|
|
36
|
-
"test": "mocha",
|
|
37
|
-
"prepublishOnly": "npm run build && npm test"
|
|
38
|
-
},
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@types/chai": "^5.0.1",
|
|
41
|
-
"@types/mocha": "^10.0.10",
|
|
42
|
-
"@types/node": "^24.0.0",
|
|
43
|
-
"chai": "^5.1.2",
|
|
44
|
-
"mocha": "^11.0.1",
|
|
45
|
-
"tsx": "^4.19.2",
|
|
46
|
-
"typescript": "^5.7.2"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "organic-protocol",
|
|
3
|
+
"version": "0.3.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
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/OrganicEconomy/organic-protocol.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/OrganicEconomy/organic-protocol/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/OrganicEconomy/organic-protocol#readme",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"PROTOCOL.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"test": "mocha",
|
|
37
|
+
"prepublishOnly": "npm run build && npm test"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/chai": "^5.0.1",
|
|
41
|
+
"@types/mocha": "^10.0.10",
|
|
42
|
+
"@types/node": "^24.0.0",
|
|
43
|
+
"chai": "^5.1.2",
|
|
44
|
+
"mocha": "^11.0.1",
|
|
45
|
+
"tsx": "^4.19.2",
|
|
46
|
+
"typescript": "^5.7.2"
|
|
47
|
+
}
|
|
48
|
+
}
|