@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.
Files changed (52) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +180 -136
  3. package/package.json +123 -28
  4. package/src/blobs/index.js +297 -0
  5. package/src/database/CLAUDE.md +3 -0
  6. package/src/database/bootstrap.js +76 -0
  7. package/src/database/dispatch.js +156 -0
  8. package/src/database/index.js +572 -0
  9. package/src/identity/CLAUDE.md +3 -0
  10. package/src/identity/index.js +232 -0
  11. package/src/index.js +20 -1
  12. package/src/lib/CLAUDE.md +3 -0
  13. package/src/lib/constants.js +24 -4
  14. package/src/lib/errors.js +150 -0
  15. package/src/lib/schema.js +58 -440
  16. package/src/lib/spec/index.js +353 -0
  17. package/src/lib/spec/schema.json +284 -0
  18. package/src/lib/utils.js +54 -49
  19. package/src/network/discovery.js +80 -0
  20. package/src/network/index.js +231 -0
  21. package/src/pairing/index.js +482 -0
  22. package/src/pairing/invite.js +199 -0
  23. package/src/rpc/client.js +45 -0
  24. package/src/rpc/index.js +141 -0
  25. package/src/rpc/server.js +45 -0
  26. package/src/storage/index.js +261 -0
  27. package/types/blobs/index.d.ts +169 -0
  28. package/types/database/bootstrap.d.ts +17 -0
  29. package/types/database/dispatch.d.ts +8 -0
  30. package/types/database/index.d.ts +329 -0
  31. package/types/identity/index.d.ts +160 -0
  32. package/types/index.d.ts +11 -0
  33. package/types/lib/constants.d.ts +13 -0
  34. package/types/lib/errors.d.ts +110 -0
  35. package/types/lib/schema.d.ts +53 -0
  36. package/types/lib/spec/index.d.ts +95 -0
  37. package/types/lib/utils.d.ts +39 -0
  38. package/types/network/discovery.d.ts +44 -0
  39. package/types/network/index.d.ts +115 -0
  40. package/types/pairing/index.d.ts +194 -0
  41. package/types/pairing/invite.d.ts +157 -0
  42. package/types/rpc/client.d.ts +18 -0
  43. package/types/rpc/index.d.ts +67 -0
  44. package/types/rpc/server.d.ts +18 -0
  45. package/types/storage/index.d.ts +163 -0
  46. package/src/lib/base.js +0 -84
  47. package/src/lib/batch.js +0 -98
  48. package/src/lib/builder.js +0 -24
  49. package/src/lib/collection.js +0 -252
  50. package/src/lib/crypto.js +0 -6
  51. package/src/lib/index.js +0 -6
  52. 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
- export * from './lib/index.js'
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'
@@ -0,0 +1,3 @@
1
+ <claude-mem-context>
2
+
3
+ </claude-mem-context>
@@ -1,4 +1,24 @@
1
- // User-facing scope names for cero-base schemas
2
- export const SCOPE_LOCAL = 'local'
3
- export const SCOPE_PRIVATE = 'private'
4
- export const SCOPE_SHARED = 'shared'
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
+ }