organic-protocol 0.1.1 → 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 CHANGED
@@ -1,131 +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": [20260719001], "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 | money unit ids (`YYYYMMDDXXX`) |
36
- | `i` | invests | invest unit ids (`YYYYMMDD9XXX`) |
37
- | `h` | signature | SECP256K1 DER signature, hex |
38
-
39
- ### 2.2 Block (`BlockWire`)
40
-
41
- ```json
42
- { "v": 1, "d": 20260719, "p": "<previous block signature>", "s": "<pk>",
43
- "m": [], "i": [], "t": 42, "r": "<merkle root>", "h": "<signature>", "x": [ …TxWire ] }
44
- ```
45
-
46
- Here `t` is the **total** (cumulative economic experience); `p` is the previous block's signature (chain link); `x` is the transaction list.
47
-
48
- ## 3. Transaction types
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>:<JSON payload>`** the prefix lives outside the JSON so any reader can dispatch (or reject an unknown version) without parsing. Current version: `1`.
63
-
64
- **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.
65
-
66
- | Type | Shape | Usage |
67
- |---|---|---|
68
- | `CT` | `OM1:CT:{"pk","url","n","e"?}` | Contact card. `n` = display name; `e` = true for an ecosystem. |
69
- | `TX` | `OM1:TX:{"tx":<PAY TxWire>,"url"}` | Offline payment, camera-to-screen. `url` = the PAYER's server, for deferred verification (§5, `tx/verify`). |
70
- | `BR` | `OM1:BR:{"pk","url","n"}` | Validation request of a new citizen. Blocks are fetched via `GET {url}/api/v1/validations/{pk}` (Phase 2) — the QR is a pointer, not a container. |
71
- | `PP` | `OM1:PP:{"tx":<PAPER TxWire>}` | Printed paper bill. |
72
-
73
- Constraints: the `tx` of a `TX` is of type PAY; the one of a `PP` is of type PAPER.
74
-
75
- ## 5. REST API `/api/v1`
76
-
77
- All routes live under `{url}/api/v1`. JSON bodies. Errors carry `{ "error": "<message>", "code": "<ApiErrorCode>"? }`.
78
-
79
- ### 5.1 Authentication schemes
80
-
81
- **Block-auth** (`PUT /users/save`, `PUT /users/sign`) — `x-signature` header = signature of the submitted block's hash, by the account key:
82
- `x-signature = signHash(block.hash(), sk)`.
83
-
84
- **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.
85
-
86
- ### 5.2 Endpoints (Phase 1)
87
-
88
- | Method & route | Auth | Body Response | Notes |
89
- |---|---|---|---|
90
- | `GET /info` | — | → `InfoResponse` | public identity card of the server |
91
- | `GET /servers` | | `ServerListEntry[]` | directory of known servers |
92
- | `POST /users/register` | — | `RegisterBody` → `RegisterResponse` | the `secretkey` arrives ENCRYPTED (opaque to the server); the password is bcrypted on arrival |
93
- | `POST /users/login` | — | `LoginBody` → `LoginResponse` | **devicetoken rotation**: the previous device is revoked |
94
- | `PUT /users/save` | block | `SaveBlockBody` → 200 | `409 DEVICE_REVOKED` when the devicetoken is no longer the active one |
95
- | `PUT /users/sign` | block | `SignBlockBody` 200 | the server signs the last block (bills, genesis) |
96
- | `POST /users/password` | timestamp | `PasswordChangeBody` → 200 | updates bcrypt + re-encrypted sk, without reading anything |
97
- | `POST /tx/send` | — | `TxSendBody` → 200 | **MANDATORY cross-verification** (§5.3) |
98
- | `GET /tx/list` | timestamp | → `TxWire[]` | pending payments for the key |
99
- | `POST /tx/verify` || `TxVerifyBody` `TxVerifyResponse` | read-only; statuses: `confirmed` / `pending` / `invalid` / `unknown-sender` |
100
- | `POST /papers/cash` | — | `PapersCashBody` → 200 | requires the full PAPER, verifies its crypto; `409 ALREADY_CASHED` |
101
- | `GET /papers/isCashed?hash=` | — | → `IsCashedResponse` \| 404 | 404 = never cashed |
102
-
103
- ### 5.3 Cross-verification (server duty)
104
-
105
- Before accepting on `tx/send` (and, in Phase 2, any ecosystem input), the server MUST:
106
-
107
- 1. verify the cryptographic validity of the transaction;
108
- 2. load the SAVED chain of the sender (`tx.s`) and validate it (`assertIsValid`);
109
- 3. verify the transaction exists in that chain's history (by its signature `h`).
110
-
111
- Without step 3, an attacker can sign a transaction carrying units they do not own. Client-side consequence: the **`pay save send` order is strict**.
112
-
113
- ### 5.4 One account = one active device
114
-
115
- 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.
116
-
117
- ### 5.5 Error codes
118
-
119
- `INVALID_TX` · `INVALID_CHAIN` · `TX_NOT_IN_CHAIN` · `UNKNOWN_SENDER` · `UNKNOWN_USER` · `INVALID_SIGNATURE` · `DEVICE_REVOKED` · `ALREADY_CASHED`
120
-
121
- ## 6. Versioning
122
-
123
- - **Protocol** (`PROTOCOL_VERSION`, `v` field of txs/blocks): bumped only on a wire-format break.
124
- - **QR** (`QR_VERSION`, `OM<v>` prefix): bumped only on a QR payload break.
125
- - **API** (`/api/v1`): a REST contract break creates `/api/v2`; `GET /info` announces the spoken versions.
126
-
127
- The three evolve independently. Every implementation MUST announce its versions via `GET /info` and refuse cleanly what it does not speak.
128
-
129
- ---
130
-
131
- *MIT license © suipotryot. Phases 2 (ecosystems, validations) and 3 (federation) will extend this document without breaking v1.*
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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
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
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.
7
+ The [organic-webapp](https://github.com/OrganicEconomy/organic-webapp) client and the [organic-webserver](https://github.com/OrganicEconomy/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
8
 
9
9
  ```bash
10
10
  npm install organic-protocol
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 validated chain (server-signed in genesis mode) */
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 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
  /**
@@ -29,6 +43,16 @@ export type IntDate = number;
29
43
  * The creation date is embedded in the id.
30
44
  */
31
45
  export type UnitId = number;
46
+ /**
47
+ * Wire representation of a `UnitId[]`: each id packed into 5 big-endian
48
+ * bytes (comfortably covers every possible money/invest id), concatenated,
49
+ * then base64-encoded. An empty array is the empty string. A plain JSON
50
+ * number array costs 12 bytes per id (11-12 decimal digits + a separator)
51
+ * for ~5 bytes of actual information — this roughly halves the size of a
52
+ * transaction or block on the wire, which matters most for QR-encoded
53
+ * paper bills. Mirrors `packUnitIds`/`unpackUnitIds` in organic-money.
54
+ */
55
+ export type PackedUnitIds = string;
32
56
  /** Compressed SECP256K1 public key, hex-encoded (33 bytes → 66 chars). */
33
57
  export type PublicKeyHex = string;
34
58
  /** DER-encoded SECP256K1 signature, hex-encoded. */
@@ -37,6 +61,14 @@ export type SignatureHex = string;
37
61
  * A transaction in wire format (short field names for compactness).
38
62
  * Field meanings: v=version, d=date, t=type, p=target, s=signer,
39
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())
40
72
  */
41
73
  export interface TxWire {
42
74
  v: number;
@@ -44,30 +76,38 @@ export interface TxWire {
44
76
  t: TxType;
45
77
  p: PublicKeyHex;
46
78
  s: PublicKeyHex;
47
- m: UnitId[];
48
- i: UnitId[];
79
+ m: PackedUnitIds;
80
+ i: PackedUnitIds;
49
81
  h: SignatureHex;
82
+ q?: number;
83
+ e?: PublicKeyHex;
84
+ x?: SignatureHex;
50
85
  }
51
86
  /**
52
87
  * A sealed (or open) block in wire format.
53
88
  * Field meanings: v=version, d=closedate, p=previous block signature,
54
89
  * 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.
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.
57
96
  */
58
97
  export interface BlockWire {
59
98
  v: number;
60
99
  d: IntDate;
61
100
  p: string;
62
101
  s: PublicKeyHex;
63
- m: UnitId[];
64
- i: UnitId[];
65
- t: number;
102
+ m: PackedUnitIds;
103
+ i: PackedUnitIds;
104
+ t: BlockType;
66
105
  r: string;
67
106
  h: SignatureHex;
68
107
  x: TxWire[];
108
+ e?: number;
69
109
  }
70
- /** 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. */
71
111
  export declare function isTxWire(o: unknown): o is TxWire;
72
112
  /** Structural check that an unknown value is a well-formed BlockWire. */
73
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
- 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. */
41
+ const isPackedUnitIds = (s) => typeof s === 'string' && /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s);
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
- return (typeof t.v === 'number' &&
53
+ if (!(typeof t.v === 'number' &&
33
54
  typeof t.d === 'number' &&
34
55
  Number.isInteger(t.d) &&
35
56
  typeof t.t === 'number' &&
@@ -37,24 +58,44 @@ export function isTxWire(o) {
37
58
  t.t <= TxType.EARN &&
38
59
  isHex(t.p) &&
39
60
  isHex(t.s) &&
40
- isUnitIdArray(t.m) &&
41
- isUnitIdArray(t.i) &&
42
- isHex(t.h));
61
+ isPackedUnitIds(t.m) &&
62
+ isPackedUnitIds(t.i) &&
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
- return (typeof b.v === 'number' &&
85
+ if (!(typeof b.v === 'number' &&
50
86
  typeof b.d === 'number' &&
51
87
  isHex(b.p) &&
52
88
  isHex(b.s) &&
53
- isUnitIdArray(b.m) &&
54
- isUnitIdArray(b.i) &&
89
+ isPackedUnitIds(b.m) &&
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,40 +1,48 @@
1
- {
2
- "name": "organic-protocol",
3
- "version": "0.1.1",
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
- }
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
+ }