@cero-base/core 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +180 -136
- package/package.json +123 -28
- package/src/blobs/index.js +297 -0
- package/src/database/CLAUDE.md +3 -0
- package/src/database/bootstrap.js +76 -0
- package/src/database/dispatch.js +156 -0
- package/src/database/index.js +572 -0
- package/src/identity/CLAUDE.md +3 -0
- package/src/identity/index.js +232 -0
- package/src/index.js +20 -1
- package/src/lib/CLAUDE.md +3 -0
- package/src/lib/constants.js +24 -4
- package/src/lib/errors.js +150 -0
- package/src/lib/schema.js +58 -440
- package/src/lib/spec/index.js +353 -0
- package/src/lib/spec/schema.json +284 -0
- package/src/lib/utils.js +54 -49
- package/src/network/discovery.js +80 -0
- package/src/network/index.js +231 -0
- package/src/pairing/index.js +482 -0
- package/src/pairing/invite.js +199 -0
- package/src/rpc/client.js +45 -0
- package/src/rpc/index.js +141 -0
- package/src/rpc/server.js +45 -0
- package/src/storage/index.js +261 -0
- package/types/blobs/index.d.ts +169 -0
- package/types/database/bootstrap.d.ts +17 -0
- package/types/database/dispatch.d.ts +8 -0
- package/types/database/index.d.ts +329 -0
- package/types/identity/index.d.ts +160 -0
- package/types/index.d.ts +11 -0
- package/types/lib/constants.d.ts +13 -0
- package/types/lib/errors.d.ts +110 -0
- package/types/lib/schema.d.ts +53 -0
- package/types/lib/spec/index.d.ts +95 -0
- package/types/lib/utils.d.ts +39 -0
- package/types/network/discovery.d.ts +44 -0
- package/types/network/index.d.ts +115 -0
- package/types/pairing/index.d.ts +194 -0
- package/types/pairing/invite.d.ts +157 -0
- package/types/rpc/client.d.ts +18 -0
- package/types/rpc/index.d.ts +67 -0
- package/types/rpc/server.d.ts +18 -0
- package/types/storage/index.d.ts +163 -0
- package/src/lib/base.js +0 -84
- package/src/lib/batch.js +0 -98
- package/src/lib/builder.js +0 -24
- package/src/lib/collection.js +0 -252
- package/src/lib/crypto.js +0 -6
- package/src/lib/index.js +0 -6
- package/src/lib/room.js +0 -145
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import sodium from 'sodium-universal'
|
|
2
|
+
import IdentityKey from 'keet-identity-key'
|
|
3
|
+
import bip39 from 'bip39-mnemonic'
|
|
4
|
+
import b4a from 'b4a'
|
|
5
|
+
|
|
6
|
+
import { CeroError } from '../lib/errors.js'
|
|
7
|
+
import { toId } from '../lib/utils.js'
|
|
8
|
+
|
|
9
|
+
const DERIVE_PATH = ['SLIP-0021', 'cero', 'v1', 'identity']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{ publicKey: Uint8Array, secretKey: Uint8Array, id: string }} KeyPair
|
|
13
|
+
* @typedef {{ publicKey: Uint8Array, secretKey: Uint8Array, encryptionKey: Uint8Array, seed: Uint8Array }} IdentityFields
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Long-lived user identity derived from a BIP-39 seed phrase. Holds the
|
|
18
|
+
* sign keypair, a symmetric encryption key and the canonical id/topic used
|
|
19
|
+
* by every other resource to identify this user.
|
|
20
|
+
*/
|
|
21
|
+
export class Identity {
|
|
22
|
+
/** @param {IdentityFields} keys */
|
|
23
|
+
constructor(keys) {
|
|
24
|
+
/**
|
|
25
|
+
* Canonical z32-encoded public key.
|
|
26
|
+
*
|
|
27
|
+
* @type {string}
|
|
28
|
+
*/
|
|
29
|
+
this.id = toId(keys.publicKey)
|
|
30
|
+
/** @type {Uint8Array} */
|
|
31
|
+
this.publicKey = keys.publicKey
|
|
32
|
+
/** @type {Uint8Array} */
|
|
33
|
+
this.secretKey = keys.secretKey
|
|
34
|
+
/**
|
|
35
|
+
* Symmetric key for encrypting per-identity payloads.
|
|
36
|
+
*
|
|
37
|
+
* @type {Uint8Array}
|
|
38
|
+
*/
|
|
39
|
+
this.encryptionKey = keys.encryptionKey
|
|
40
|
+
/**
|
|
41
|
+
* Hash of the public key — used as the swarm topic.
|
|
42
|
+
*
|
|
43
|
+
* @type {Uint8Array}
|
|
44
|
+
*/
|
|
45
|
+
this.topic = topicOf(keys.publicKey)
|
|
46
|
+
/**
|
|
47
|
+
* Original 16- or 32-byte entropy.
|
|
48
|
+
*
|
|
49
|
+
* @type {Uint8Array}
|
|
50
|
+
*/
|
|
51
|
+
this.seed = keys.seed
|
|
52
|
+
Object.freeze(this)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detached Ed25519 signature over `message`.
|
|
57
|
+
*
|
|
58
|
+
* @param {Uint8Array} message
|
|
59
|
+
* @returns {Uint8Array}
|
|
60
|
+
*/
|
|
61
|
+
sign(message) {
|
|
62
|
+
const sig = b4a.alloc(sodium.crypto_sign_BYTES)
|
|
63
|
+
sodium.crypto_sign_detached(sig, message, this.secretKey)
|
|
64
|
+
return sig
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Verify a signature against this identity's public key.
|
|
69
|
+
*
|
|
70
|
+
* @param {Uint8Array} message
|
|
71
|
+
* @param {Uint8Array} signature
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
verify(message, signature) {
|
|
75
|
+
return sodium.crypto_sign_verify_detached(signature, message, this.publicKey)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render the underlying seed as a BIP-39 mnemonic phrase.
|
|
80
|
+
*
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
toPhrase() {
|
|
84
|
+
return bip39.entropyToMnemonic(this.seed)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build an identity from 16- or 32-byte entropy.
|
|
89
|
+
*
|
|
90
|
+
* @param {Uint8Array} seed
|
|
91
|
+
* @returns {Promise<Identity>}
|
|
92
|
+
*/
|
|
93
|
+
static async fromSeed(seed) {
|
|
94
|
+
if (!Identity.isSeed(seed)) throw CeroError.INVALID('seed must be a 16- or 32-byte buffer')
|
|
95
|
+
const mnemonic = bip39.entropyToMnemonic(seed)
|
|
96
|
+
const id = await IdentityKey.from({ mnemonic })
|
|
97
|
+
const { publicKey, secretKey } = id.identityKeyPair
|
|
98
|
+
const encryptionKey = id.keyChain.getSymmetricKey([...DERIVE_PATH, 'encryption'])
|
|
99
|
+
return new Identity({ publicKey, secretKey, encryptionKey, seed })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build an identity from a BIP-39 mnemonic.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} phrase
|
|
106
|
+
* @returns {Promise<Identity>}
|
|
107
|
+
*/
|
|
108
|
+
static async fromPhrase(phrase) {
|
|
109
|
+
if (!Identity.isPhrase(phrase)) throw CeroError.INVALID('phrase must be a valid BIP39 mnemonic')
|
|
110
|
+
return Identity.fromSeed(bip39.mnemonicToEntropy(phrase))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate a fresh identity from CSPRNG entropy.
|
|
115
|
+
*
|
|
116
|
+
* @param {{ words?: 12 | 24 }} [opts]
|
|
117
|
+
* @returns {Promise<Identity>}
|
|
118
|
+
*/
|
|
119
|
+
static async generate({ words = 12 } = {}) {
|
|
120
|
+
return Identity.fromSeed(Identity.randomSeed(words))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a random BIP-39 mnemonic.
|
|
125
|
+
*
|
|
126
|
+
* @param {12 | 24} [words]
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
static genPhrase(words = 12) {
|
|
130
|
+
return bip39.entropyToMnemonic(Identity.randomSeed(words))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Phrase → seed entropy.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} phrase
|
|
137
|
+
* @returns {Uint8Array}
|
|
138
|
+
*/
|
|
139
|
+
static toSeed(phrase) {
|
|
140
|
+
return bip39.mnemonicToEntropy(phrase)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Seed entropy → phrase.
|
|
145
|
+
*
|
|
146
|
+
* @param {Uint8Array} seed
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
static toPhrase(seed) {
|
|
150
|
+
return bip39.entropyToMnemonic(seed)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Validate that a value is a 16- or 32-byte buffer.
|
|
155
|
+
*
|
|
156
|
+
* @param {any} x
|
|
157
|
+
* @returns {x is Uint8Array}
|
|
158
|
+
*/
|
|
159
|
+
static isSeed(x) {
|
|
160
|
+
if (!b4a.isBuffer(x) && !(x instanceof Uint8Array)) return false
|
|
161
|
+
return x.length === 16 || x.length === 32
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Validate that a string is a BIP-39 mnemonic.
|
|
166
|
+
*
|
|
167
|
+
* @param {unknown} x
|
|
168
|
+
* @returns {x is string}
|
|
169
|
+
*/
|
|
170
|
+
static isPhrase(x) {
|
|
171
|
+
if (typeof x !== 'string') return false
|
|
172
|
+
try {
|
|
173
|
+
bip39.mnemonicToEntropy(x)
|
|
174
|
+
return true
|
|
175
|
+
} catch {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Verify a signature against an arbitrary public key.
|
|
182
|
+
*
|
|
183
|
+
* @param {Uint8Array} publicKey
|
|
184
|
+
* @param {Uint8Array} message
|
|
185
|
+
* @param {Uint8Array} signature
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
static verify(publicKey, message, signature) {
|
|
189
|
+
return sodium.crypto_sign_verify_detached(signature, message, publicKey)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Generate a fresh Ed25519 keypair (for devices, blind invites, etc.).
|
|
194
|
+
*
|
|
195
|
+
* @returns {KeyPair}
|
|
196
|
+
*/
|
|
197
|
+
static randomKeyPair() {
|
|
198
|
+
const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
|
|
199
|
+
const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)
|
|
200
|
+
sodium.crypto_sign_keypair(publicKey, secretKey)
|
|
201
|
+
return { publicKey, secretKey, id: toId(publicKey) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* N bytes from a CSPRNG.
|
|
206
|
+
*
|
|
207
|
+
* @param {number} [n]
|
|
208
|
+
* @returns {Uint8Array}
|
|
209
|
+
*/
|
|
210
|
+
static randomBytes(n = 32) {
|
|
211
|
+
const buf = b4a.alloc(n)
|
|
212
|
+
sodium.randombytes_buf(buf)
|
|
213
|
+
return buf
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Fresh seed entropy sized for the chosen mnemonic length.
|
|
218
|
+
*
|
|
219
|
+
* @param {12 | 24} [words]
|
|
220
|
+
* @returns {Uint8Array}
|
|
221
|
+
*/
|
|
222
|
+
static randomSeed(words = 12) {
|
|
223
|
+
if (words !== 12 && words !== 24) throw new RangeError('words must be 12 or 24')
|
|
224
|
+
return Identity.randomBytes((words / 3) * 4)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function topicOf(publicKey) {
|
|
229
|
+
const topic = b4a.alloc(32)
|
|
230
|
+
sodium.crypto_generichash(topic, publicKey)
|
|
231
|
+
return topic
|
|
232
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @cero-base/core — primitives for building p2p applications. This barrel
|
|
3
|
+
* re-exports the canonical surface: identity, storage, network, database,
|
|
4
|
+
* blobs, rpc, pairing, the schema DSL, the error class, and shared utils.
|
|
5
|
+
*
|
|
6
|
+
* Tree-shake-friendly: subpath imports (`@cero-base/core/identity`, etc.)
|
|
7
|
+
* are also available for consumers that need a smaller graph.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { Identity } from './identity/index.js'
|
|
11
|
+
export { Storage } from './storage/index.js'
|
|
12
|
+
export { Network } from './network/index.js'
|
|
13
|
+
export { Database } from './database/index.js'
|
|
14
|
+
export { Blobs } from './blobs/index.js'
|
|
15
|
+
export { RPCServer, RPCClient, bindCodec } from './rpc/index.js'
|
|
16
|
+
export { Pairing } from './pairing/index.js'
|
|
17
|
+
export { Invite } from './pairing/invite.js'
|
|
18
|
+
export { t, schema } from './lib/schema.js'
|
|
19
|
+
export { genId, toId, toKey, isKeyId, subscribe } from './lib/utils.js'
|
|
20
|
+
export { CeroError } from './lib/errors.js'
|
package/src/lib/constants.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
//
|
|
2
|
-
export const
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
1
|
+
// Schema kinds
|
|
2
|
+
export const SINGLE = 'single'
|
|
3
|
+
export const COLLECTION = 'collection'
|
|
4
|
+
export const HANDLE = 'handle'
|
|
5
|
+
export const ACTION = 'action'
|
|
6
|
+
|
|
7
|
+
// Network topic modes
|
|
8
|
+
export const ACTIVE = 'active'
|
|
9
|
+
export const PASSIVE = 'passive'
|
|
10
|
+
|
|
11
|
+
// Storage backends
|
|
12
|
+
export const ROCKS = 'rocks'
|
|
13
|
+
export const BEE = 'bee'
|
|
14
|
+
|
|
15
|
+
// Roles
|
|
16
|
+
export const OWNER = 'owner'
|
|
17
|
+
export const WRITE = 'write'
|
|
18
|
+
export const READ = 'read'
|
|
19
|
+
|
|
20
|
+
// Identity / hypercore namespace
|
|
21
|
+
export const NAMESPACE = 'cero'
|
|
22
|
+
|
|
23
|
+
// Internal counter collection (assigns monotonic `index` to every row in every collection)
|
|
24
|
+
export const COUNTERS = 'counters'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical error class. Every instance carries a stable `code` field which
|
|
3
|
+
* is also prefixed to the message, so logs stay self-identifying. Static
|
|
4
|
+
* factories produce errors with the right code + a sensible message.
|
|
5
|
+
*/
|
|
6
|
+
export class CeroError extends Error {
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} code Stable identifier (e.g. `REQUIRED`, `EXPIRED`).
|
|
9
|
+
* @param {string} [message] Human-readable detail; appended after the code.
|
|
10
|
+
* @param {Record<string, unknown> | null} [extras] Extra fields copied onto the instance.
|
|
11
|
+
*/
|
|
12
|
+
constructor(code, message, extras = null) {
|
|
13
|
+
super(message ? `${code}: ${message}` : code)
|
|
14
|
+
this.isCeroError = true
|
|
15
|
+
this.code = code
|
|
16
|
+
if (extras) Object.assign(this, extras)
|
|
17
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, CeroError)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get name() {
|
|
21
|
+
return 'CeroError'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Type-guard for `err instanceof CeroError` that survives realm boundaries.
|
|
26
|
+
*
|
|
27
|
+
* @param {any} err
|
|
28
|
+
* @returns {err is CeroError}
|
|
29
|
+
*/
|
|
30
|
+
static isCeroError(err) {
|
|
31
|
+
return err?.isCeroError === true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Missing required argument.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} name
|
|
38
|
+
*/
|
|
39
|
+
static REQUIRED(name) {
|
|
40
|
+
return new CeroError('REQUIRED', `${name} is required`)
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Argument failed validation.
|
|
44
|
+
*
|
|
45
|
+
* @param {string} msg
|
|
46
|
+
*/
|
|
47
|
+
static INVALID(msg) {
|
|
48
|
+
return new CeroError('INVALID', msg)
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Operation on a closed resource.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} resource
|
|
54
|
+
*/
|
|
55
|
+
static CLOSED(resource) {
|
|
56
|
+
return new CeroError('CLOSED', `${resource} is closed`)
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Operation before `ready()` resolved.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} resource
|
|
62
|
+
* @param {string} name
|
|
63
|
+
*/
|
|
64
|
+
static NOT_READY(resource, name) {
|
|
65
|
+
return new CeroError('NOT_READY', `${resource} is not ready — await ${name}.ready() first`)
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resource has been destroyed.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} resource
|
|
71
|
+
*/
|
|
72
|
+
static DESTROYED(resource) {
|
|
73
|
+
return new CeroError('DESTROYED', `${resource} is destroyed`)
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Lookup failed for a typed id.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} kind
|
|
79
|
+
* @param {string} id
|
|
80
|
+
*/
|
|
81
|
+
static UNKNOWN(kind, id) {
|
|
82
|
+
return new CeroError('UNKNOWN', `unknown ${kind}: ${id}`)
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Write attempted on a read-only handle.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} resource
|
|
88
|
+
*/
|
|
89
|
+
static NOT_WRITABLE(resource) {
|
|
90
|
+
return new CeroError('NOT_WRITABLE', `${resource} is not writable`)
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Async operation exceeded its deadline.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} what
|
|
96
|
+
*/
|
|
97
|
+
static TIMED_OUT(what) {
|
|
98
|
+
return new CeroError('TIMED_OUT', `${what} timed out`)
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Feature is not yet implemented.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} what
|
|
104
|
+
*/
|
|
105
|
+
static UNSUPPORTED(what) {
|
|
106
|
+
return new CeroError('UNSUPPORTED', `${what} not yet supported`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Invite payload failed to parse or verify.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} [msg]
|
|
113
|
+
*/
|
|
114
|
+
static INVALID_INVITE(msg = 'invalid invite') {
|
|
115
|
+
return new CeroError('INVALID_INVITE', msg)
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Invite is past its TTL.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} [msg]
|
|
121
|
+
*/
|
|
122
|
+
static EXPIRED(msg = 'invite expired') {
|
|
123
|
+
return new CeroError('EXPIRED', msg)
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Host refused the join — `reason` is exposed on the instance.
|
|
127
|
+
*
|
|
128
|
+
* @param {string | null} [reason]
|
|
129
|
+
* @param {string} [msg]
|
|
130
|
+
*/
|
|
131
|
+
static DENIED(reason = null, msg = 'pairing denied') {
|
|
132
|
+
return new CeroError('DENIED', msg, { reason })
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Pairing handshake did not complete in time.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} [msg]
|
|
138
|
+
*/
|
|
139
|
+
static TIMEOUT(msg = 'pairing timed out') {
|
|
140
|
+
return new CeroError('TIMEOUT', msg)
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Underlying swarm/transport failure.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} [msg]
|
|
146
|
+
*/
|
|
147
|
+
static NETWORK_ERROR(msg = 'network error') {
|
|
148
|
+
return new CeroError('NETWORK_ERROR', msg)
|
|
149
|
+
}
|
|
150
|
+
}
|