organic-protocol 0.1.0 → 0.2.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 +69 -65
- package/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/wire.d.ts +14 -4
- package/dist/wire.js +5 -5
- package/package.json +9 -1
package/PROTOCOL.md
CHANGED
|
@@ -1,51 +1,55 @@
|
|
|
1
|
-
# Organic Money —
|
|
1
|
+
# Organic Money — Inter-instance Protocol (v1)
|
|
2
2
|
|
|
3
|
-
|
|
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
4
|
|
|
5
|
-
|
|
5
|
+
Protocol version: **1**. QR format version: **1** (independent, see §4).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1.
|
|
9
|
+
## 1. Federated identity
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
A public key alone is not enough to reach someone in a decentralized network. An **identity** is always the pair:
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
|
-
{ "pk": "<
|
|
14
|
+
{ "pk": "<compressed SECP256K1 public key, hex>", "url": "<root URL of the referent server>" }
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
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
18
|
|
|
19
|
-
## 2.
|
|
19
|
+
## 2. Wire formats
|
|
20
20
|
|
|
21
21
|
### 2.1 Transaction (`TxWire`)
|
|
22
22
|
|
|
23
23
|
```json
|
|
24
|
-
{ "v": 1, "d": 20260719, "t": 3, "p": "<pk
|
|
25
|
-
"m":
|
|
24
|
+
{ "v": 1, "d": 20260719, "t": 3, "p": "<target pk>", "s": "<signer pk>",
|
|
25
|
+
"m": "BLeiCZk=", "i": "", "h": "<DER signature, hex>" }
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
|
28
|
+
| Field | Long name | Content |
|
|
29
29
|
|---|---|---|
|
|
30
|
-
| `v` | version | version
|
|
31
|
-
| `d` | date |
|
|
32
|
-
| `t` | type | type
|
|
33
|
-
| `p` | target |
|
|
34
|
-
| `s` | signer |
|
|
35
|
-
| `m` | money |
|
|
36
|
-
| `i` | invests |
|
|
37
|
-
| `h` | signature |
|
|
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
38
|
|
|
39
|
-
### 2.2
|
|
39
|
+
### 2.2 Block (`BlockWire`)
|
|
40
40
|
|
|
41
41
|
```json
|
|
42
|
-
{ "v": 1, "d": 20260719, "p": "<
|
|
43
|
-
"m":
|
|
42
|
+
{ "v": 1, "d": 20260719, "p": "<previous block signature>", "s": "<pk>",
|
|
43
|
+
"m": "", "i": "", "t": 42, "r": "<merkle root>", "h": "<signature>", "x": [ …TxWire ] }
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`t`
|
|
46
|
+
Here `t` is the **total** (cumulative economic experience); `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).
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
### 2.3 Packed unit ids (`m`, `i`)
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
## 3. Transaction types
|
|
49
53
|
|
|
50
54
|
| # | Type | # | Type |
|
|
51
55
|
|---|---|---|---|
|
|
@@ -59,73 +63,73 @@ L'`url` est l'URL **racine** du serveur (sans `/api`, sans slash final), normali
|
|
|
59
63
|
|
|
60
64
|
## 4. QR codes
|
|
61
65
|
|
|
62
|
-
Format
|
|
66
|
+
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
67
|
|
|
64
|
-
**
|
|
68
|
+
**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
69
|
|
|
66
|
-
| Type |
|
|
70
|
+
| Type | Shape | Usage |
|
|
67
71
|
|---|---|---|
|
|
68
|
-
| `CT` | `OM1:CT:{"pk","url","n","e"?}` |
|
|
69
|
-
| `TX` | `OM1:TX:{"tx":<TxWire
|
|
70
|
-
| `BR` | `OM1:BR:{"pk","url","n"}` |
|
|
71
|
-
| `PP` | `OM1:PP:{"tx":<TxWire
|
|
72
|
+
| `CT` | `OM1:CT:{"pk","url","n","e"?}` | Contact card. `n` = display name; `e` = true for an ecosystem. |
|
|
73
|
+
| `TX` | `OM1:TX:{"tx":<PAY TxWire>,"url"}` | Offline payment, camera-to-screen. `url` = the PAYER's server, for deferred verification (§5, `tx/verify`). |
|
|
74
|
+
| `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. |
|
|
75
|
+
| `PP` | `OM1:PP:{"tx":<PAPER TxWire>}` | Printed paper bill. |
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
Constraints: the `tx` of a `TX` is of type PAY; the one of a `PP` is of type PAPER.
|
|
74
78
|
|
|
75
|
-
## 5. API
|
|
79
|
+
## 5. REST API `/api/v1`
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
All routes live under `{url}/api/v1`. JSON bodies. Errors carry `{ "error": "<message>", "code": "<ApiErrorCode>"? }`.
|
|
78
82
|
|
|
79
|
-
### 5.1
|
|
83
|
+
### 5.1 Authentication schemes
|
|
80
84
|
|
|
81
|
-
**Block-auth** (`PUT /users/save`, `PUT /users/sign`) —
|
|
85
|
+
**Block-auth** (`PUT /users/save`, `PUT /users/sign`) — `x-signature` header = signature of the submitted block's hash, by the account key:
|
|
82
86
|
`x-signature = signHash(block.hash(), sk)`.
|
|
83
87
|
|
|
84
|
-
**Timestamp-auth** (`GET /tx/list`, `POST /users/password`, …) —
|
|
88
|
+
**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
89
|
|
|
86
90
|
### 5.2 Endpoints (Phase 1)
|
|
87
91
|
|
|
88
|
-
|
|
|
92
|
+
| Method & route | Auth | Body → Response | Notes |
|
|
89
93
|
|---|---|---|---|
|
|
90
|
-
| `GET /info` | — | → `InfoResponse` |
|
|
91
|
-
| `GET /servers` | — | → `ServerListEntry[]` |
|
|
92
|
-
| `POST /users/register` | — | `RegisterBody` → `RegisterResponse` |
|
|
93
|
-
| `POST /users/login` | — | `LoginBody` → `LoginResponse` | **rotation
|
|
94
|
-
| `PUT /users/save` | block | `SaveBlockBody` → 200 | `409 DEVICE_REVOKED`
|
|
95
|
-
| `PUT /users/sign` | block | `SignBlockBody` → 200 |
|
|
96
|
-
| `POST /users/password` | timestamp | `PasswordChangeBody` → 200 |
|
|
97
|
-
| `POST /tx/send` | — | `TxSendBody` → 200 | **
|
|
98
|
-
| `GET /tx/list` | timestamp | → `TxWire[]` |
|
|
99
|
-
| `POST /tx/verify` | — | `TxVerifyBody` → `TxVerifyResponse` |
|
|
100
|
-
| `POST /papers/cash` | — | `PapersCashBody` → 200 |
|
|
101
|
-
| `GET /papers/isCashed?hash=` | — | → `IsCashedResponse` \| 404 | 404 =
|
|
94
|
+
| `GET /info` | — | → `InfoResponse` | public identity card of the server |
|
|
95
|
+
| `GET /servers` | — | → `ServerListEntry[]` | directory of known servers |
|
|
96
|
+
| `POST /users/register` | — | `RegisterBody` → `RegisterResponse` | the `secretkey` arrives ENCRYPTED (opaque to the server); the password is bcrypted on arrival |
|
|
97
|
+
| `POST /users/login` | — | `LoginBody` → `LoginResponse` | **devicetoken rotation**: the previous device is revoked |
|
|
98
|
+
| `PUT /users/save` | block | `SaveBlockBody` → 200 | `409 DEVICE_REVOKED` when the devicetoken is no longer the active one |
|
|
99
|
+
| `PUT /users/sign` | block | `SignBlockBody` → 200 | the server signs the last block (bills, genesis) |
|
|
100
|
+
| `POST /users/password` | timestamp | `PasswordChangeBody` → 200 | updates bcrypt + re-encrypted sk, without reading anything |
|
|
101
|
+
| `POST /tx/send` | — | `TxSendBody` → 200 | **MANDATORY cross-verification** (§5.3) |
|
|
102
|
+
| `GET /tx/list` | timestamp | → `TxWire[]` | pending payments for the key |
|
|
103
|
+
| `POST /tx/verify` | — | `TxVerifyBody` → `TxVerifyResponse` | read-only; statuses: `confirmed` / `pending` / `invalid` / `unknown-sender` |
|
|
104
|
+
| `POST /papers/cash` | — | `PapersCashBody` → 200 | requires the full PAPER, verifies its crypto; `409 ALREADY_CASHED` |
|
|
105
|
+
| `GET /papers/isCashed?hash=` | — | → `IsCashedResponse` \| 404 | 404 = never cashed |
|
|
102
106
|
|
|
103
|
-
### 5.3
|
|
107
|
+
### 5.3 Cross-verification (server duty)
|
|
104
108
|
|
|
105
|
-
|
|
109
|
+
Before accepting on `tx/send` (and, in Phase 2, any ecosystem input), the server MUST:
|
|
106
110
|
|
|
107
|
-
1.
|
|
108
|
-
2.
|
|
109
|
-
3.
|
|
111
|
+
1. verify the cryptographic validity of the transaction;
|
|
112
|
+
2. load the SAVED chain of the sender (`tx.s`) and validate it (`assertIsValid`);
|
|
113
|
+
3. verify the transaction exists in that chain's history (by its signature `h`).
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
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
116
|
|
|
113
|
-
### 5.4
|
|
117
|
+
### 5.4 One account = one active device
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
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
120
|
|
|
117
|
-
### 5.5
|
|
121
|
+
### 5.5 Error codes
|
|
118
122
|
|
|
119
123
|
`INVALID_TX` · `INVALID_CHAIN` · `TX_NOT_IN_CHAIN` · `UNKNOWN_SENDER` · `UNKNOWN_USER` · `INVALID_SIGNATURE` · `DEVICE_REVOKED` · `ALREADY_CASHED`
|
|
120
124
|
|
|
121
|
-
## 6.
|
|
125
|
+
## 6. Versioning
|
|
122
126
|
|
|
123
|
-
- **
|
|
124
|
-
- **QR** (`QR_VERSION`,
|
|
125
|
-
- **API** (`/api/v1`)
|
|
127
|
+
- **Protocol** (`PROTOCOL_VERSION`, `v` field of txs/blocks): bumped only on a wire-format break.
|
|
128
|
+
- **QR** (`QR_VERSION`, `OM<v>` prefix): bumped only on a QR payload break.
|
|
129
|
+
- **API** (`/api/v1`): a REST contract break creates `/api/v2`; `GET /info` announces the spoken versions.
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
The three evolve independently. Every implementation MUST announce its versions via `GET /info` and refuse cleanly what it does not speak.
|
|
128
132
|
|
|
129
133
|
---
|
|
130
134
|
|
|
131
|
-
*
|
|
135
|
+
*MIT license — © suipotryot. Phases 2 (ecosystems, validations) and 3 (federation) will extend this document without breaking v1.*
|
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/
|
|
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/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, 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
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/wire.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export type IntDate = number;
|
|
|
29
29
|
* The creation date is embedded in the id.
|
|
30
30
|
*/
|
|
31
31
|
export type UnitId = number;
|
|
32
|
+
/**
|
|
33
|
+
* Wire representation of a `UnitId[]`: each id packed into 5 big-endian
|
|
34
|
+
* bytes (comfortably covers every possible money/invest id), concatenated,
|
|
35
|
+
* then base64-encoded. An empty array is the empty string. A plain JSON
|
|
36
|
+
* number array costs 12 bytes per id (11-12 decimal digits + a separator)
|
|
37
|
+
* for ~5 bytes of actual information — this roughly halves the size of a
|
|
38
|
+
* transaction or block on the wire, which matters most for QR-encoded
|
|
39
|
+
* paper bills. Mirrors `packUnitIds`/`unpackUnitIds` in organic-money.
|
|
40
|
+
*/
|
|
41
|
+
export type PackedUnitIds = string;
|
|
32
42
|
/** Compressed SECP256K1 public key, hex-encoded (33 bytes → 66 chars). */
|
|
33
43
|
export type PublicKeyHex = string;
|
|
34
44
|
/** DER-encoded SECP256K1 signature, hex-encoded. */
|
|
@@ -44,8 +54,8 @@ export interface TxWire {
|
|
|
44
54
|
t: TxType;
|
|
45
55
|
p: PublicKeyHex;
|
|
46
56
|
s: PublicKeyHex;
|
|
47
|
-
m:
|
|
48
|
-
i:
|
|
57
|
+
m: PackedUnitIds;
|
|
58
|
+
i: PackedUnitIds;
|
|
49
59
|
h: SignatureHex;
|
|
50
60
|
}
|
|
51
61
|
/**
|
|
@@ -60,8 +70,8 @@ export interface BlockWire {
|
|
|
60
70
|
d: IntDate;
|
|
61
71
|
p: string;
|
|
62
72
|
s: PublicKeyHex;
|
|
63
|
-
m:
|
|
64
|
-
i:
|
|
73
|
+
m: PackedUnitIds;
|
|
74
|
+
i: PackedUnitIds;
|
|
65
75
|
t: number;
|
|
66
76
|
r: string;
|
|
67
77
|
h: SignatureHex;
|
package/dist/wire.js
CHANGED
|
@@ -23,7 +23,7 @@ export var TxType;
|
|
|
23
23
|
TxType[TxType["EARN"] = 13] = "EARN";
|
|
24
24
|
})(TxType || (TxType = {}));
|
|
25
25
|
const isHex = (s) => typeof s === 'string' && /^[0-9a-fA-F]*$/.test(s);
|
|
26
|
-
const
|
|
26
|
+
const isPackedUnitIds = (s) => typeof s === 'string' && /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s);
|
|
27
27
|
/** Structural check that an unknown value is a well-formed TxWire. */
|
|
28
28
|
export function isTxWire(o) {
|
|
29
29
|
if (typeof o !== 'object' || o === null)
|
|
@@ -37,8 +37,8 @@ export function isTxWire(o) {
|
|
|
37
37
|
t.t <= TxType.EARN &&
|
|
38
38
|
isHex(t.p) &&
|
|
39
39
|
isHex(t.s) &&
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
isPackedUnitIds(t.m) &&
|
|
41
|
+
isPackedUnitIds(t.i) &&
|
|
42
42
|
isHex(t.h));
|
|
43
43
|
}
|
|
44
44
|
/** Structural check that an unknown value is a well-formed BlockWire. */
|
|
@@ -50,8 +50,8 @@ export function isBlockWire(o) {
|
|
|
50
50
|
typeof b.d === 'number' &&
|
|
51
51
|
isHex(b.p) &&
|
|
52
52
|
isHex(b.s) &&
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
isPackedUnitIds(b.m) &&
|
|
54
|
+
isPackedUnitIds(b.i) &&
|
|
55
55
|
typeof b.t === 'number' &&
|
|
56
56
|
isHex(b.r) &&
|
|
57
57
|
isHex(b.h) &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "organic-protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"organic-economy",
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
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",
|
|
13
21
|
"type": "module",
|
|
14
22
|
"main": "./dist/index.js",
|
|
15
23
|
"types": "./dist/index.d.ts",
|