@nexus-code/remote-protocol 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCOUNT_KEYS.md +83 -0
- package/LICENSE +21 -0
- package/README.md +27 -2
- package/dist/account.d.ts +261 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +60 -0
- package/dist/account.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/info.d.ts +46 -0
- package/dist/info.d.ts.map +1 -0
- package/dist/info.js +17 -0
- package/dist/info.js.map +1 -0
- package/dist/relay.d.ts +273 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +77 -0
- package/dist/relay.js.map +1 -0
- package/dist/rpc.d.ts +6270 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +225 -0
- package/dist/rpc.js.map +1 -0
- package/dist/sealed.d.ts +230 -0
- package/dist/sealed.d.ts.map +1 -0
- package/dist/sealed.js +61 -0
- package/dist/sealed.js.map +1 -0
- package/dist/view.d.ts +1120 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/view.js +187 -0
- package/dist/view.js.map +1 -0
- package/dist/voice.d.ts +139 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +58 -0
- package/dist/voice.js.map +1 -0
- package/package.json +36 -4
- package/src/account.ts +69 -0
- package/src/index.ts +18 -0
- package/src/info.ts +18 -0
- package/src/relay.ts +96 -0
- package/src/rpc.ts +277 -0
- package/src/sealed.ts +69 -0
- package/src/view.ts +214 -0
- package/src/voice.ts +67 -0
package/ACCOUNT_KEYS.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# The account key (draft, v1)
|
|
2
|
+
|
|
3
|
+
A user's devices seal what they send each other with a key only those devices hold. The relay passes the sealed messages on but cannot open them. A new device gets the key when the user signs in with Google and types the **account password**. The server keeps the key locked with that password and never sees the password or the key.
|
|
4
|
+
|
|
5
|
+
The design is open on purpose: its safety rests on the keys, not on the code being hidden.
|
|
6
|
+
|
|
7
|
+
## Keys
|
|
8
|
+
|
|
9
|
+
| Key | What it is | Where it lives |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Account key (AK)** | 32 random bytes, made by the first device. `kid` is its version, from 1. | Each signed-in device, in Electron `safeStorage`. Never on the server unlocked. |
|
|
12
|
+
| **Room key** | `HKDF-SHA256(ikm = AK, salt = empty, info = "nexus-remote/room-key/v1", 32 bytes)` | Made on each device from AK. Seals every frame's content. |
|
|
13
|
+
| **Password key (KEK)** | `Argon2id(password, salt, ops, mem) → 32 bytes`. libsodium's `crypto_pwhash`, `ALG_ARGON2ID13`. New bundles use ops 3 and 64 MiB. The password is NFKC-normalized first. | Made only while unlocking, then dropped. |
|
|
14
|
+
|
|
15
|
+
## The locked bundle (on the server)
|
|
16
|
+
|
|
17
|
+
`KeyBundleSchema` in `src/account.ts`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"v": 1,
|
|
22
|
+
"kid": 1,
|
|
23
|
+
"kdf": { "alg": "argon2id13", "ops": 3, "mem": 67108864, "salt": "<16 bytes>" },
|
|
24
|
+
"nonce": "<24 bytes>",
|
|
25
|
+
"box": "<XChaCha20-Poly1305(KEK, AK, aad = bundleAadOf(userId, kid))>"
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- The associated data, `nexus-remote/account-key/v1|<userId>|<kid>`, ties the bundle to its user and key version. The server cannot pass one user's bundle off as another's, or an old key as the current one.
|
|
30
|
+
- A wrong password fails to open the box, because the AEAD check fails. No separate verifier is stored.
|
|
31
|
+
- The bundle keeps its own KDF settings, so new bundles can raise the cost without breaking old ones.
|
|
32
|
+
|
|
33
|
+
## Sealed frames
|
|
34
|
+
|
|
35
|
+
- Content: `XChaCha20-Poly1305(room key, json, nonce = 24 random bytes, aad = aadOf({from, to, seq, kid}))`.
|
|
36
|
+
- `kid` in each frame names the AK version it was sealed with.
|
|
37
|
+
- A device that gets a frame with a `kid` it does not hold fetches the bundle again and asks for the password (see "Reset").
|
|
38
|
+
|
|
39
|
+
## Flows
|
|
40
|
+
|
|
41
|
+
**First device** (`GET /account/key-bundle` → 404)
|
|
42
|
+
1. The user sets the account password (typed twice) after signing in.
|
|
43
|
+
2. The device makes AK (`kid` 1) and a salt, then derives KEK and seals AK.
|
|
44
|
+
3. It sends `PUT /account/key-bundle` with `{ bundle, expected: null }`. A 409 means another device made one first: fetch it and unlock it instead.
|
|
45
|
+
4. It keeps AK in `safeStorage`.
|
|
46
|
+
|
|
47
|
+
**Another device** (`GET` → 200)
|
|
48
|
+
1. The user types the account password.
|
|
49
|
+
2. The device derives KEK with the bundle's `kdf` and opens the box.
|
|
50
|
+
3. It keeps AK in `safeStorage`. A wrong password says so and asks again. Argon2id's cost is what slows guessing.
|
|
51
|
+
|
|
52
|
+
**Changing the password** (a device holding AK, after the user types the current password again)
|
|
53
|
+
- Seal the same AK, same `kid`, under a new salt and the new password.
|
|
54
|
+
- `PUT` with `expected = <version>`.
|
|
55
|
+
- Other devices keep working; they hold AK already.
|
|
56
|
+
|
|
57
|
+
**Forgotten password: reset**
|
|
58
|
+
1. The device makes a new AK (`kid + 1`) and seals it with a new password.
|
|
59
|
+
2. `PUT` with `expected = <version>`.
|
|
60
|
+
3. Other devices find frames with the new `kid`, fetch the bundle, and ask for the new password.
|
|
61
|
+
4. Nothing is lost: the server keeps no history, only the sealed bundle.
|
|
62
|
+
|
|
63
|
+
**Removing a device**
|
|
64
|
+
- `DELETE /devices/:id` ends its sign-in and closes its socket. AK stays the same.
|
|
65
|
+
- For a lost or stolen device, the user should also reset, so the device's copy of AK opens nothing new.
|
|
66
|
+
|
|
67
|
+
## What the server needs to provide
|
|
68
|
+
|
|
69
|
+
- `GET /account/key-bundle` → `200 { bundle, version }` or `404`.
|
|
70
|
+
- `PUT /account/key-bundle` with body `{ bundle, expected }` → `200 { version }`, or `409` when `expected` is not the current version (`null` when one exists already).
|
|
71
|
+
- Check the bundle against `KeyBundleSchema` and cap its size (it is well under 1 KB). Store it opaque. Return it only to a signed-in device of the same user.
|
|
72
|
+
- Rate-limit the GET per user. Anyone who gets the bundle can guess passwords offline, so keep it only for the account's own devices.
|
|
73
|
+
|
|
74
|
+
## On the device
|
|
75
|
+
|
|
76
|
+
- AK and the session token go in Electron `safeStorage`.
|
|
77
|
+
- On Linux without a keyring, `safeStorage` falls back to plain text (`basic_text`). There, AK is not written to disk; the password is asked at each start.
|
|
78
|
+
- Crypto: `libsodium-wrappers-sumo` (wasm: `crypto_pwhash`, XChaCha20-Poly1305) and Node's `crypto.hkdfSync`. No native modules, so the three OSes and the Store build stay simple.
|
|
79
|
+
|
|
80
|
+
## Later (not v1)
|
|
81
|
+
|
|
82
|
+
- Per-device X25519 keys, signed with AK. These allow approving a new device from the phone or a QR code, and removing a device without a reset.
|
|
83
|
+
- Forward secrecy for sealed frames (pairwise ephemeral keys).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 moreih29
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nexus-code/remote-protocol
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The protocol between a NexusCode user's devices and the relay that connects them, as zod schemas.
|
|
4
|
+
|
|
5
|
+
There are two layers:
|
|
6
|
+
|
|
7
|
+
- **Relay frames** (`relay.ts`): what a device and its account's room on the relay send each other. The relay reads these: who a message is for, its sequence number, and the account key version it was sealed with. It cannot read the message itself.
|
|
8
|
+
- **Sealed messages** (`sealed.ts`, `rpc.ts`, `view.ts`): what a sealed frame carries between two devices of the account.
|
|
9
|
+
- A `hello` that says which protocol version and capabilities a device serves.
|
|
10
|
+
- JSON-RPC 2.0 requests, answers and notifications: list and watch sessions, read and drive chats, read terminals.
|
|
11
|
+
- The chat view: the conversation as the serving device's app already built it, trimmed for drawing.
|
|
12
|
+
|
|
13
|
+
Beside them:
|
|
14
|
+
|
|
15
|
+
- **The account key's bundle** (`account.ts`): the account key locked with the account password, as the server keeps it. See `ACCOUNT_KEYS.md`.
|
|
16
|
+
- **The voice socket** (`voice.ts`): audio to the server, text back while the user speaks.
|
|
17
|
+
|
|
18
|
+
Sealing is XChaCha20-Poly1305. The key is the room key, derived from the account key. The associated data comes from `aadOf()` and binds a message to its routing.
|
|
19
|
+
|
|
20
|
+
Until it is published, other repos use it by a local path, e.g. `"@nexus-code/remote-protocol": "file:../nexus-code/packages/remote-protocol"`, after `npm run build` here: its exports point at `dist`. They need `zod` 3.25 or later.
|
|
21
|
+
|
|
22
|
+
## Use and publish
|
|
23
|
+
|
|
24
|
+
Published as `@nexus-code/remote-protocol`: ES modules with their type
|
|
25
|
+
declarations in `dist`, and `zod` 3.25 or later as a peer. The desktop app
|
|
26
|
+
imports `src` directly; `npm run build` (run by `prepublishOnly`) makes
|
|
27
|
+
`dist`. Relative imports in `src` end in `.js`, as Node's ES modules need
|
|
28
|
+
them.
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account key, locked with the account password: the server keeps the
|
|
3
|
+
* locked bundle and hands it to the account's devices, which open it on
|
|
4
|
+
* their own (see ../ACCOUNT_KEYS.md). The server never sees the password,
|
|
5
|
+
* the key, or anything made from them; it only checks the bundle's shape.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
/** The associated data the key is sealed with: the user's id and the key's version, so a bundle cannot pass for another's. */
|
|
9
|
+
export declare function bundleAadOf(userId: string, kid: number): string;
|
|
10
|
+
/** Argon2id's settings, as libsodium's crypto_pwhash takes them (ALG_ARGON2ID13). */
|
|
11
|
+
export declare const KdfSchema: z.ZodObject<{
|
|
12
|
+
alg: z.ZodLiteral<"argon2id13">;
|
|
13
|
+
/** crypto_pwhash's opslimit. */
|
|
14
|
+
ops: z.ZodNumber;
|
|
15
|
+
/** crypto_pwhash's memlimit, in bytes. */
|
|
16
|
+
mem: z.ZodNumber;
|
|
17
|
+
/** 16 random bytes. */
|
|
18
|
+
salt: z.ZodString;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
alg: "argon2id13";
|
|
21
|
+
ops: number;
|
|
22
|
+
mem: number;
|
|
23
|
+
salt: string;
|
|
24
|
+
}, {
|
|
25
|
+
alg: "argon2id13";
|
|
26
|
+
ops: number;
|
|
27
|
+
mem: number;
|
|
28
|
+
salt: string;
|
|
29
|
+
}>;
|
|
30
|
+
export type Kdf = z.infer<typeof KdfSchema>;
|
|
31
|
+
/** The settings a new bundle is locked with; a bundle keeps its own, so these can grow. */
|
|
32
|
+
export declare const DEFAULT_KDF: {
|
|
33
|
+
readonly alg: "argon2id13";
|
|
34
|
+
readonly ops: 3;
|
|
35
|
+
readonly mem: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The account key (32 bytes) sealed with XChaCha20-Poly1305 under the key
|
|
39
|
+
* the password makes with `kdf`, with bundleAadOf(userId, kid) as the
|
|
40
|
+
* associated data. `kid` is the account key's version, which sealed frames
|
|
41
|
+
* name; it goes up when the key is made anew.
|
|
42
|
+
*/
|
|
43
|
+
export declare const KeyBundleSchema: z.ZodObject<{
|
|
44
|
+
v: z.ZodLiteral<1>;
|
|
45
|
+
kid: z.ZodNumber;
|
|
46
|
+
kdf: z.ZodObject<{
|
|
47
|
+
alg: z.ZodLiteral<"argon2id13">;
|
|
48
|
+
/** crypto_pwhash's opslimit. */
|
|
49
|
+
ops: z.ZodNumber;
|
|
50
|
+
/** crypto_pwhash's memlimit, in bytes. */
|
|
51
|
+
mem: z.ZodNumber;
|
|
52
|
+
/** 16 random bytes. */
|
|
53
|
+
salt: z.ZodString;
|
|
54
|
+
}, "strip", z.ZodTypeAny, {
|
|
55
|
+
alg: "argon2id13";
|
|
56
|
+
ops: number;
|
|
57
|
+
mem: number;
|
|
58
|
+
salt: string;
|
|
59
|
+
}, {
|
|
60
|
+
alg: "argon2id13";
|
|
61
|
+
ops: number;
|
|
62
|
+
mem: number;
|
|
63
|
+
salt: string;
|
|
64
|
+
}>;
|
|
65
|
+
/** 24 random bytes. */
|
|
66
|
+
nonce: z.ZodString;
|
|
67
|
+
box: z.ZodString;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
kid: number;
|
|
70
|
+
nonce: string;
|
|
71
|
+
box: string;
|
|
72
|
+
v: 1;
|
|
73
|
+
kdf: {
|
|
74
|
+
alg: "argon2id13";
|
|
75
|
+
ops: number;
|
|
76
|
+
mem: number;
|
|
77
|
+
salt: string;
|
|
78
|
+
};
|
|
79
|
+
}, {
|
|
80
|
+
kid: number;
|
|
81
|
+
nonce: string;
|
|
82
|
+
box: string;
|
|
83
|
+
v: 1;
|
|
84
|
+
kdf: {
|
|
85
|
+
alg: "argon2id13";
|
|
86
|
+
ops: number;
|
|
87
|
+
mem: number;
|
|
88
|
+
salt: string;
|
|
89
|
+
};
|
|
90
|
+
}>;
|
|
91
|
+
export type KeyBundle = z.infer<typeof KeyBundleSchema>;
|
|
92
|
+
/** GET /account/key-bundle: the bundle and its version on the server, or 404 before there is one. */
|
|
93
|
+
export declare const KeyBundleResponseSchema: z.ZodObject<{
|
|
94
|
+
bundle: z.ZodObject<{
|
|
95
|
+
v: z.ZodLiteral<1>;
|
|
96
|
+
kid: z.ZodNumber;
|
|
97
|
+
kdf: z.ZodObject<{
|
|
98
|
+
alg: z.ZodLiteral<"argon2id13">;
|
|
99
|
+
/** crypto_pwhash's opslimit. */
|
|
100
|
+
ops: z.ZodNumber;
|
|
101
|
+
/** crypto_pwhash's memlimit, in bytes. */
|
|
102
|
+
mem: z.ZodNumber;
|
|
103
|
+
/** 16 random bytes. */
|
|
104
|
+
salt: z.ZodString;
|
|
105
|
+
}, "strip", z.ZodTypeAny, {
|
|
106
|
+
alg: "argon2id13";
|
|
107
|
+
ops: number;
|
|
108
|
+
mem: number;
|
|
109
|
+
salt: string;
|
|
110
|
+
}, {
|
|
111
|
+
alg: "argon2id13";
|
|
112
|
+
ops: number;
|
|
113
|
+
mem: number;
|
|
114
|
+
salt: string;
|
|
115
|
+
}>;
|
|
116
|
+
/** 24 random bytes. */
|
|
117
|
+
nonce: z.ZodString;
|
|
118
|
+
box: z.ZodString;
|
|
119
|
+
}, "strip", z.ZodTypeAny, {
|
|
120
|
+
kid: number;
|
|
121
|
+
nonce: string;
|
|
122
|
+
box: string;
|
|
123
|
+
v: 1;
|
|
124
|
+
kdf: {
|
|
125
|
+
alg: "argon2id13";
|
|
126
|
+
ops: number;
|
|
127
|
+
mem: number;
|
|
128
|
+
salt: string;
|
|
129
|
+
};
|
|
130
|
+
}, {
|
|
131
|
+
kid: number;
|
|
132
|
+
nonce: string;
|
|
133
|
+
box: string;
|
|
134
|
+
v: 1;
|
|
135
|
+
kdf: {
|
|
136
|
+
alg: "argon2id13";
|
|
137
|
+
ops: number;
|
|
138
|
+
mem: number;
|
|
139
|
+
salt: string;
|
|
140
|
+
};
|
|
141
|
+
}>;
|
|
142
|
+
/** Counts the bundle's writes, for the next PUT to say which one it replaces. */
|
|
143
|
+
version: z.ZodNumber;
|
|
144
|
+
}, "strip", z.ZodTypeAny, {
|
|
145
|
+
bundle: {
|
|
146
|
+
kid: number;
|
|
147
|
+
nonce: string;
|
|
148
|
+
box: string;
|
|
149
|
+
v: 1;
|
|
150
|
+
kdf: {
|
|
151
|
+
alg: "argon2id13";
|
|
152
|
+
ops: number;
|
|
153
|
+
mem: number;
|
|
154
|
+
salt: string;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
version: number;
|
|
158
|
+
}, {
|
|
159
|
+
bundle: {
|
|
160
|
+
kid: number;
|
|
161
|
+
nonce: string;
|
|
162
|
+
box: string;
|
|
163
|
+
v: 1;
|
|
164
|
+
kdf: {
|
|
165
|
+
alg: "argon2id13";
|
|
166
|
+
ops: number;
|
|
167
|
+
mem: number;
|
|
168
|
+
salt: string;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
version: number;
|
|
172
|
+
}>;
|
|
173
|
+
export type KeyBundleResponse = z.infer<typeof KeyBundleResponseSchema>;
|
|
174
|
+
/**
|
|
175
|
+
* PUT /account/key-bundle. `expected` is the version it replaces, or null
|
|
176
|
+
* for the first bundle; the server answers 409 when another write came
|
|
177
|
+
* first. A new password locks the same key (same kid); a reset puts a new
|
|
178
|
+
* key (kid + 1), which the account's other devices take once they are told
|
|
179
|
+
* the new password.
|
|
180
|
+
*/
|
|
181
|
+
export declare const KeyBundlePutSchema: z.ZodObject<{
|
|
182
|
+
bundle: z.ZodObject<{
|
|
183
|
+
v: z.ZodLiteral<1>;
|
|
184
|
+
kid: z.ZodNumber;
|
|
185
|
+
kdf: z.ZodObject<{
|
|
186
|
+
alg: z.ZodLiteral<"argon2id13">;
|
|
187
|
+
/** crypto_pwhash's opslimit. */
|
|
188
|
+
ops: z.ZodNumber;
|
|
189
|
+
/** crypto_pwhash's memlimit, in bytes. */
|
|
190
|
+
mem: z.ZodNumber;
|
|
191
|
+
/** 16 random bytes. */
|
|
192
|
+
salt: z.ZodString;
|
|
193
|
+
}, "strip", z.ZodTypeAny, {
|
|
194
|
+
alg: "argon2id13";
|
|
195
|
+
ops: number;
|
|
196
|
+
mem: number;
|
|
197
|
+
salt: string;
|
|
198
|
+
}, {
|
|
199
|
+
alg: "argon2id13";
|
|
200
|
+
ops: number;
|
|
201
|
+
mem: number;
|
|
202
|
+
salt: string;
|
|
203
|
+
}>;
|
|
204
|
+
/** 24 random bytes. */
|
|
205
|
+
nonce: z.ZodString;
|
|
206
|
+
box: z.ZodString;
|
|
207
|
+
}, "strip", z.ZodTypeAny, {
|
|
208
|
+
kid: number;
|
|
209
|
+
nonce: string;
|
|
210
|
+
box: string;
|
|
211
|
+
v: 1;
|
|
212
|
+
kdf: {
|
|
213
|
+
alg: "argon2id13";
|
|
214
|
+
ops: number;
|
|
215
|
+
mem: number;
|
|
216
|
+
salt: string;
|
|
217
|
+
};
|
|
218
|
+
}, {
|
|
219
|
+
kid: number;
|
|
220
|
+
nonce: string;
|
|
221
|
+
box: string;
|
|
222
|
+
v: 1;
|
|
223
|
+
kdf: {
|
|
224
|
+
alg: "argon2id13";
|
|
225
|
+
ops: number;
|
|
226
|
+
mem: number;
|
|
227
|
+
salt: string;
|
|
228
|
+
};
|
|
229
|
+
}>;
|
|
230
|
+
expected: z.ZodNullable<z.ZodNumber>;
|
|
231
|
+
}, "strip", z.ZodTypeAny, {
|
|
232
|
+
expected: number | null;
|
|
233
|
+
bundle: {
|
|
234
|
+
kid: number;
|
|
235
|
+
nonce: string;
|
|
236
|
+
box: string;
|
|
237
|
+
v: 1;
|
|
238
|
+
kdf: {
|
|
239
|
+
alg: "argon2id13";
|
|
240
|
+
ops: number;
|
|
241
|
+
mem: number;
|
|
242
|
+
salt: string;
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
}, {
|
|
246
|
+
expected: number | null;
|
|
247
|
+
bundle: {
|
|
248
|
+
kid: number;
|
|
249
|
+
nonce: string;
|
|
250
|
+
box: string;
|
|
251
|
+
v: 1;
|
|
252
|
+
kdf: {
|
|
253
|
+
alg: "argon2id13";
|
|
254
|
+
ops: number;
|
|
255
|
+
mem: number;
|
|
256
|
+
salt: string;
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
}>;
|
|
260
|
+
export type KeyBundlePut = z.infer<typeof KeyBundlePutSchema>;
|
|
261
|
+
//# sourceMappingURL=account.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,8HAA8H;AAC9H,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,qFAAqF;AACrF,eAAO,MAAM,SAAS;;IAEpB,gCAAgC;;IAEhC,0CAA0C;;IAM1C,uBAAuB;;;;;;;;;;;;EAEvB,CAAC;AACH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAE5C,2FAA2F;AAC3F,eAAO,MAAM,WAAW;;;;CAAgE,CAAC;AAEzF;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;QAtB1B,gCAAgC;;QAEhC,0CAA0C;;QAM1C,uBAAuB;;;;;;;;;;;;;IAkBvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;EAGvB,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,qGAAqG;AACrG,eAAO,MAAM,uBAAuB;;;;;;YAjClC,gCAAgC;;YAEhC,0CAA0C;;YAM1C,uBAAuB;;;;;;;;;;;;;QAkBvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;IASvB,iFAAiF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEjF,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB;;;;;;YA/C7B,gCAAgC;;YAEhC,0CAA0C;;YAM1C,uBAAuB;;;;;;;;;;;;;QAkBvB,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
package/dist/account.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account key, locked with the account password: the server keeps the
|
|
3
|
+
* locked bundle and hands it to the account's devices, which open it on
|
|
4
|
+
* their own (see ../ACCOUNT_KEYS.md). The server never sees the password,
|
|
5
|
+
* the key, or anything made from them; it only checks the bundle's shape.
|
|
6
|
+
*/
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { Base64UrlSchema } from "./relay.js";
|
|
9
|
+
/** The associated data the key is sealed with: the user's id and the key's version, so a bundle cannot pass for another's. */
|
|
10
|
+
export function bundleAadOf(userId, kid) {
|
|
11
|
+
return `nexus-remote/account-key/v1|${userId}|${kid}`;
|
|
12
|
+
}
|
|
13
|
+
/** Argon2id's settings, as libsodium's crypto_pwhash takes them (ALG_ARGON2ID13). */
|
|
14
|
+
export const KdfSchema = z.object({
|
|
15
|
+
alg: z.literal("argon2id13"),
|
|
16
|
+
/** crypto_pwhash's opslimit. */
|
|
17
|
+
ops: z.number().int().min(1).max(10),
|
|
18
|
+
/** crypto_pwhash's memlimit, in bytes. */
|
|
19
|
+
mem: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.min(16 * 1024 * 1024)
|
|
23
|
+
.max(1024 * 1024 * 1024),
|
|
24
|
+
/** 16 random bytes. */
|
|
25
|
+
salt: Base64UrlSchema,
|
|
26
|
+
});
|
|
27
|
+
/** The settings a new bundle is locked with; a bundle keeps its own, so these can grow. */
|
|
28
|
+
export const DEFAULT_KDF = { alg: "argon2id13", ops: 3, mem: 64 * 1024 * 1024 };
|
|
29
|
+
/**
|
|
30
|
+
* The account key (32 bytes) sealed with XChaCha20-Poly1305 under the key
|
|
31
|
+
* the password makes with `kdf`, with bundleAadOf(userId, kid) as the
|
|
32
|
+
* associated data. `kid` is the account key's version, which sealed frames
|
|
33
|
+
* name; it goes up when the key is made anew.
|
|
34
|
+
*/
|
|
35
|
+
export const KeyBundleSchema = z.object({
|
|
36
|
+
v: z.literal(1),
|
|
37
|
+
kid: z.number().int().positive(),
|
|
38
|
+
kdf: KdfSchema,
|
|
39
|
+
/** 24 random bytes. */
|
|
40
|
+
nonce: Base64UrlSchema,
|
|
41
|
+
box: Base64UrlSchema,
|
|
42
|
+
});
|
|
43
|
+
/** GET /account/key-bundle: the bundle and its version on the server, or 404 before there is one. */
|
|
44
|
+
export const KeyBundleResponseSchema = z.object({
|
|
45
|
+
bundle: KeyBundleSchema,
|
|
46
|
+
/** Counts the bundle's writes, for the next PUT to say which one it replaces. */
|
|
47
|
+
version: z.number().int().positive(),
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* PUT /account/key-bundle. `expected` is the version it replaces, or null
|
|
51
|
+
* for the first bundle; the server answers 409 when another write came
|
|
52
|
+
* first. A new password locks the same key (same kid); a reset puts a new
|
|
53
|
+
* key (kid + 1), which the account's other devices take once they are told
|
|
54
|
+
* the new password.
|
|
55
|
+
*/
|
|
56
|
+
export const KeyBundlePutSchema = z.object({
|
|
57
|
+
bundle: KeyBundleSchema,
|
|
58
|
+
expected: z.number().int().positive().nullable(),
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,8HAA8H;AAC9H,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,GAAW;IACrD,OAAO,+BAA+B,MAAM,IAAI,GAAG,EAAE,CAAC;AACxD,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAC5B,gCAAgC;IAChC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACpC,0CAA0C;IAC1C,GAAG,EAAE,CAAC;SACH,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;SACrB,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,uBAAuB;IACvB,IAAI,EAAE,eAAe;CACtB,CAAC,CAAC;AAGH,2FAA2F;AAC3F,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAW,CAAC;AAEzF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACf,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAChC,GAAG,EAAE,SAAS;IACd,uBAAuB;IACvB,KAAK,EAAE,eAAe;IACtB,GAAG,EAAE,eAAe;CACrB,CAAC,CAAC;AAGH,qGAAqG;AACrG,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,eAAe;IACvB,iFAAiF;IACjF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAGH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,MAAM,EAAE,eAAe;IACvB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACjD,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexusCode's remote protocol, in two layers, and the account key's bundle:
|
|
3
|
+
* - ./relay: the frames between a device and its account's room on the
|
|
4
|
+
* relay, which the relay reads and routes;
|
|
5
|
+
* - ./sealed, ./rpc, ./view: what a sealed frame carries between two
|
|
6
|
+
* devices of the account, which only they can open;
|
|
7
|
+
* - ./account: the account key, locked with the account password, as the
|
|
8
|
+
* server keeps it;
|
|
9
|
+
* - ./voice: the voice socket, speech to text while the user speaks;
|
|
10
|
+
* - ./info: what the server offers (GET /info).
|
|
11
|
+
*/
|
|
12
|
+
export * from "./account.js";
|
|
13
|
+
export * from "./info.js";
|
|
14
|
+
export * from "./relay.js";
|
|
15
|
+
export * from "./rpc.js";
|
|
16
|
+
export * from "./sealed.js";
|
|
17
|
+
export * from "./view.js";
|
|
18
|
+
export * from "./voice.js";
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexusCode's remote protocol, in two layers, and the account key's bundle:
|
|
3
|
+
* - ./relay: the frames between a device and its account's room on the
|
|
4
|
+
* relay, which the relay reads and routes;
|
|
5
|
+
* - ./sealed, ./rpc, ./view: what a sealed frame carries between two
|
|
6
|
+
* devices of the account, which only they can open;
|
|
7
|
+
* - ./account: the account key, locked with the account password, as the
|
|
8
|
+
* server keeps it;
|
|
9
|
+
* - ./voice: the voice socket, speech to text while the user speaks;
|
|
10
|
+
* - ./info: what the server offers (GET /info).
|
|
11
|
+
*/
|
|
12
|
+
export * from "./account.js";
|
|
13
|
+
export * from "./info.js";
|
|
14
|
+
export * from "./relay.js";
|
|
15
|
+
export * from "./rpc.js";
|
|
16
|
+
export * from "./sealed.js";
|
|
17
|
+
export * from "./view.js";
|
|
18
|
+
export * from "./voice.js";
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
|
package/dist/info.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /info, signed in or not: what the server offers, so a device shows
|
|
3
|
+
* only what works there.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export declare const ServerInfoSchema: z.ZodObject<{
|
|
7
|
+
/**
|
|
8
|
+
* The sign-ins the server has, each its own /auth/<name>/start: "google",
|
|
9
|
+
* "github", and "dev" only on a server at localhost. A device offers one
|
|
10
|
+
* button for each it knows.
|
|
11
|
+
*/
|
|
12
|
+
signIn: z.ZodArray<z.ZodString, "many">;
|
|
13
|
+
relay: z.ZodNullable<z.ZodObject<{
|
|
14
|
+
version: z.ZodNumber;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
version: number;
|
|
17
|
+
}, {
|
|
18
|
+
version: number;
|
|
19
|
+
}>>;
|
|
20
|
+
/** Null where the server has no speech-to-text provider: no microphone then. */
|
|
21
|
+
voice: z.ZodNullable<z.ZodObject<{
|
|
22
|
+
version: z.ZodNumber;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
version: number;
|
|
25
|
+
}, {
|
|
26
|
+
version: number;
|
|
27
|
+
}>>;
|
|
28
|
+
}, "strip", z.ZodTypeAny, {
|
|
29
|
+
signIn: string[];
|
|
30
|
+
relay: {
|
|
31
|
+
version: number;
|
|
32
|
+
} | null;
|
|
33
|
+
voice: {
|
|
34
|
+
version: number;
|
|
35
|
+
} | null;
|
|
36
|
+
}, {
|
|
37
|
+
signIn: string[];
|
|
38
|
+
relay: {
|
|
39
|
+
version: number;
|
|
40
|
+
} | null;
|
|
41
|
+
voice: {
|
|
42
|
+
version: number;
|
|
43
|
+
} | null;
|
|
44
|
+
}>;
|
|
45
|
+
export type ServerInfo = z.infer<typeof ServerInfoSchema>;
|
|
46
|
+
//# sourceMappingURL=info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"info.d.ts","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,gBAAgB;IAC3B;;;;OAIG;;;;;;;;;IAGH,gFAAgF;;;;;;;;;;;;;;;;;;;;;;;;EAEhF,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
|
package/dist/info.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /info, signed in or not: what the server offers, so a device shows
|
|
3
|
+
* only what works there.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const ServerInfoSchema = z.object({
|
|
7
|
+
/**
|
|
8
|
+
* The sign-ins the server has, each its own /auth/<name>/start: "google",
|
|
9
|
+
* "github", and "dev" only on a server at localhost. A device offers one
|
|
10
|
+
* button for each it knows.
|
|
11
|
+
*/
|
|
12
|
+
signIn: z.array(z.string()),
|
|
13
|
+
relay: z.object({ version: z.number().int() }).nullable(),
|
|
14
|
+
/** Null where the server has no speech-to-text provider: no microphone then. */
|
|
15
|
+
voice: z.object({ version: z.number().int() }).nullable(),
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=info.js.map
|
package/dist/info.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"info.js","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC;;;;OAIG;IACH,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzD,gFAAgF;IAChF,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE;CAC1D,CAAC,CAAC"}
|