@xnetjs/core 0.0.2
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 +21 -0
- package/README.md +66 -0
- package/dist/index.d.ts +780 -0
- package/dist/index.js +343 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Smothers
|
|
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
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @xnetjs/core
|
|
2
|
+
|
|
3
|
+
Core types, content addressing, and permission primitives for xNet. This is the leaf package -- it has no internal `@xnetjs/*` dependencies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @xnetjs/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Content addressing** -- BLAKE3-based CIDs (`cid:blake3:{hash}`), Merkle trees
|
|
14
|
+
- **Signed updates** -- Vector clocks, signed update types for causal ordering
|
|
15
|
+
- **Snapshots** -- Point-in-time snapshot types for state persistence
|
|
16
|
+
- **Verification** -- Fork detection, update chain verification
|
|
17
|
+
- **DID resolution** -- Pluggable DID resolver interface
|
|
18
|
+
- **Query federation** -- Types for cross-hub federated queries
|
|
19
|
+
- **Permissions** -- Role-based access control (RBAC), capabilities
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { hashContent, createContentId, verifyContent, buildMerkleTree } from '@xnetjs/core'
|
|
25
|
+
|
|
26
|
+
// Hash content with BLAKE3
|
|
27
|
+
const hash = hashContent(new Uint8Array([1, 2, 3]))
|
|
28
|
+
|
|
29
|
+
// Create a content-addressed ID
|
|
30
|
+
const cid = createContentId(data)
|
|
31
|
+
|
|
32
|
+
// Verify content integrity
|
|
33
|
+
const isValid = verifyContent(cid, data)
|
|
34
|
+
|
|
35
|
+
// Build a Merkle tree
|
|
36
|
+
const tree = buildMerkleTree(chunks)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { detectFork, verifyUpdateChain } from '@xnetjs/core'
|
|
41
|
+
|
|
42
|
+
// Verify an update chain
|
|
43
|
+
const valid = verifyUpdateChain(updates)
|
|
44
|
+
|
|
45
|
+
// Detect forks in update history
|
|
46
|
+
const fork = detectFork(chain1, chain2)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Modules
|
|
50
|
+
|
|
51
|
+
| Module | Description |
|
|
52
|
+
| ----------------- | ---------------------------------- |
|
|
53
|
+
| `content.ts` | Content addressing, CID creation |
|
|
54
|
+
| `hashing.ts` | BLAKE3 hashing, Merkle trees |
|
|
55
|
+
| `snapshots.ts` | Snapshot types and utilities |
|
|
56
|
+
| `updates.ts` | Signed updates, vector clocks |
|
|
57
|
+
| `verification.ts` | Fork detection, chain verification |
|
|
58
|
+
| `resolution.ts` | DID resolver interface |
|
|
59
|
+
| `federation.ts` | Federated query types |
|
|
60
|
+
| `permissions.ts` | Roles, capabilities, RBAC |
|
|
61
|
+
|
|
62
|
+
## Testing
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm --filter @xnetjs/core test
|
|
66
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content addressing types for xNet
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Content ID format: cid:blake3:{hash}
|
|
6
|
+
*/
|
|
7
|
+
type ContentId = `cid:blake3:${string}`;
|
|
8
|
+
/**
|
|
9
|
+
* A chunk of content with its hash
|
|
10
|
+
*/
|
|
11
|
+
interface ContentChunk {
|
|
12
|
+
data: Uint8Array;
|
|
13
|
+
hash: string;
|
|
14
|
+
size: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Merkle tree node for document structure
|
|
18
|
+
*/
|
|
19
|
+
interface MerkleNode {
|
|
20
|
+
hash: string;
|
|
21
|
+
children?: string[];
|
|
22
|
+
data?: Uint8Array;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Complete content tree for a document
|
|
26
|
+
*/
|
|
27
|
+
interface ContentTree {
|
|
28
|
+
rootHash: string;
|
|
29
|
+
nodes: Map<string, MerkleNode>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Content resolver interface
|
|
33
|
+
*/
|
|
34
|
+
interface ContentResolver {
|
|
35
|
+
/** Get content by CID */
|
|
36
|
+
get(cid: ContentId): Promise<Uint8Array | null>;
|
|
37
|
+
/** Store content, returns CID */
|
|
38
|
+
put(data: Uint8Array): Promise<ContentId>;
|
|
39
|
+
/** Verify content matches CID */
|
|
40
|
+
verify(cid: ContentId, data: Uint8Array): boolean;
|
|
41
|
+
/** Build Merkle tree from chunks */
|
|
42
|
+
buildTree(chunks: ContentChunk[]): ContentTree;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* BLAKE3 content hashing for xNet
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Hash content using BLAKE3
|
|
51
|
+
*/
|
|
52
|
+
declare function hashContent(data: Uint8Array): string;
|
|
53
|
+
/**
|
|
54
|
+
* Create a ContentId from a hash
|
|
55
|
+
*/
|
|
56
|
+
declare function createContentId(hash: string): ContentId;
|
|
57
|
+
/**
|
|
58
|
+
* Parse a ContentId to extract the hash
|
|
59
|
+
*/
|
|
60
|
+
declare function parseContentId(cid: ContentId): string;
|
|
61
|
+
/**
|
|
62
|
+
* Verify content matches a CID
|
|
63
|
+
*/
|
|
64
|
+
declare function verifyContent(cid: ContentId, data: Uint8Array): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Create a content chunk from data
|
|
67
|
+
*/
|
|
68
|
+
declare function createChunk(data: Uint8Array): ContentChunk;
|
|
69
|
+
/**
|
|
70
|
+
* Build a Merkle tree from content chunks
|
|
71
|
+
*/
|
|
72
|
+
declare function buildMerkleTree(chunks: ContentChunk[]): ContentTree;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Signed update types for xNet CRDT synchronization
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* Vector clock for tracking causality
|
|
79
|
+
*/
|
|
80
|
+
interface VectorClock {
|
|
81
|
+
[peerId: string]: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A signed CRDT update with chain linkage
|
|
85
|
+
*/
|
|
86
|
+
interface SignedUpdate {
|
|
87
|
+
update: Uint8Array;
|
|
88
|
+
parentHash: string;
|
|
89
|
+
updateHash: string;
|
|
90
|
+
authorDID: string;
|
|
91
|
+
signature: Uint8Array;
|
|
92
|
+
timestamp: number;
|
|
93
|
+
vectorClock: VectorClock;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Represents a fork in the update chain
|
|
97
|
+
*/
|
|
98
|
+
interface Fork {
|
|
99
|
+
commonAncestor: string;
|
|
100
|
+
branch1: SignedUpdate[];
|
|
101
|
+
branch2: SignedUpdate[];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Update chain status
|
|
105
|
+
*/
|
|
106
|
+
interface ChainStatus {
|
|
107
|
+
valid: boolean;
|
|
108
|
+
errors: string[];
|
|
109
|
+
forks: Fork[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compare two vector clocks
|
|
113
|
+
* Returns:
|
|
114
|
+
* -1 if a < b (a happened before b)
|
|
115
|
+
* 0 if a || b (concurrent)
|
|
116
|
+
* 1 if a > b (a happened after b)
|
|
117
|
+
*/
|
|
118
|
+
declare function compareVectorClocks(a: VectorClock, b: VectorClock): -1 | 0 | 1;
|
|
119
|
+
/**
|
|
120
|
+
* Check if a vector clock progression is valid
|
|
121
|
+
* The author's clock should increment by exactly 1
|
|
122
|
+
*/
|
|
123
|
+
declare function isValidProgression(prev: VectorClock, next: VectorClock, authorId: string): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Merge two vector clocks (take max of each component)
|
|
126
|
+
*/
|
|
127
|
+
declare function mergeVectorClocks(a: VectorClock, b: VectorClock): VectorClock;
|
|
128
|
+
/**
|
|
129
|
+
* Increment a vector clock for a given peer
|
|
130
|
+
*/
|
|
131
|
+
declare function incrementVectorClock(clock: VectorClock, peerId: string): VectorClock;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Snapshot types and logic for xNet CRDT persistence
|
|
135
|
+
*/
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Triggers for when to create a new snapshot
|
|
139
|
+
*/
|
|
140
|
+
interface SnapshotTriggers {
|
|
141
|
+
updateCount: number;
|
|
142
|
+
timeInterval: number;
|
|
143
|
+
storagePressure: number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A snapshot represents a compressed CRDT state at a point in time
|
|
147
|
+
*/
|
|
148
|
+
interface Snapshot {
|
|
149
|
+
id: string;
|
|
150
|
+
documentId: string;
|
|
151
|
+
stateVector: Uint8Array;
|
|
152
|
+
compressedState: Uint8Array;
|
|
153
|
+
timestamp: number;
|
|
154
|
+
creatorDID: string;
|
|
155
|
+
signature: Uint8Array;
|
|
156
|
+
contentId: ContentId;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* What's needed to load a document
|
|
160
|
+
*/
|
|
161
|
+
interface DocumentLoad {
|
|
162
|
+
snapshot?: Snapshot;
|
|
163
|
+
updatesSinceSnapshot: SignedUpdate[];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Default snapshot triggers
|
|
167
|
+
*/
|
|
168
|
+
declare const DEFAULT_SNAPSHOT_TRIGGERS: SnapshotTriggers;
|
|
169
|
+
/**
|
|
170
|
+
* Determine if a new snapshot should be created
|
|
171
|
+
*/
|
|
172
|
+
declare function shouldCreateSnapshot(updateCount: number, lastSnapshotTime: number, storageUsed: number, storageTotal: number, triggers?: SnapshotTriggers): boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Calculate the effective state vector after applying updates
|
|
175
|
+
*/
|
|
176
|
+
declare function mergeStateVectors(base: Uint8Array, _updates: SignedUpdate[]): Uint8Array;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Update verification types and interfaces
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Interface for verifying signed updates
|
|
184
|
+
*/
|
|
185
|
+
interface UpdateVerifier {
|
|
186
|
+
/** Verify update signature and chain linkage */
|
|
187
|
+
verify(update: SignedUpdate, publicKey: Uint8Array): Promise<boolean>;
|
|
188
|
+
/** Detect forks in update chain */
|
|
189
|
+
detectFork(updates: SignedUpdate[]): Fork | null;
|
|
190
|
+
/** Check vector clock progression */
|
|
191
|
+
isValidProgression(prev: VectorClock, next: VectorClock, authorId: string): boolean;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Detect forks in an update chain
|
|
195
|
+
* A fork occurs when two updates have the same parent
|
|
196
|
+
*/
|
|
197
|
+
declare function detectFork(updates: SignedUpdate[]): Fork | null;
|
|
198
|
+
/**
|
|
199
|
+
* Verify an update chain
|
|
200
|
+
*/
|
|
201
|
+
declare function verifyUpdateChain(updates: SignedUpdate[], getPublicKey: (did: string) => Promise<Uint8Array>, verifySignature: (data: Uint8Array, signature: Uint8Array, publicKey: Uint8Array) => boolean): Promise<ChainStatus>;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* DID resolution types for xNet peer discovery
|
|
205
|
+
*/
|
|
206
|
+
/**
|
|
207
|
+
* Location of a peer on the network
|
|
208
|
+
*/
|
|
209
|
+
interface PeerLocation {
|
|
210
|
+
multiaddr: string;
|
|
211
|
+
lastSeen: number;
|
|
212
|
+
latency?: number;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Result of resolving a DID
|
|
216
|
+
*/
|
|
217
|
+
interface DIDResolution {
|
|
218
|
+
did: string;
|
|
219
|
+
publicKey: Uint8Array;
|
|
220
|
+
locations: PeerLocation[];
|
|
221
|
+
lastUpdated: number;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Strategy for resolving DIDs
|
|
225
|
+
*/
|
|
226
|
+
type ResolutionStrategy = 'local-cache' | 'connected-peers' | 'dht' | 'bootstrap';
|
|
227
|
+
/**
|
|
228
|
+
* Interface for DID resolution
|
|
229
|
+
*/
|
|
230
|
+
interface DIDResolver {
|
|
231
|
+
/** Resolve DID to locations and public key */
|
|
232
|
+
resolve(did: string): Promise<DIDResolution | null>;
|
|
233
|
+
/** Publish own location */
|
|
234
|
+
publish(did: string, locations: PeerLocation[]): Promise<void>;
|
|
235
|
+
/** Check cache without network */
|
|
236
|
+
getCached(did: string): DIDResolution | null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Bootstrap peers for initial network discovery
|
|
240
|
+
*/
|
|
241
|
+
declare const BOOTSTRAP_PEERS: readonly ["/dns4/bootstrap1.xnet.io/tcp/4001/p2p/12D3KooWBootstrap1", "/dns4/bootstrap2.xnet.io/tcp/4001/p2p/12D3KooWBootstrap2"];
|
|
242
|
+
/**
|
|
243
|
+
* DHT configuration for peer discovery
|
|
244
|
+
*/
|
|
245
|
+
declare const DHT_CONFIG: {
|
|
246
|
+
readonly protocol: "/xnet/kad/1.0.0";
|
|
247
|
+
readonly replicationFactor: 20;
|
|
248
|
+
readonly refreshInterval: number;
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Resolution cache configuration
|
|
252
|
+
*/
|
|
253
|
+
declare const RESOLUTION_CACHE_CONFIG: {
|
|
254
|
+
readonly maxEntries: 1000;
|
|
255
|
+
readonly ttl: number;
|
|
256
|
+
readonly staleWhileRevalidate: number;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Parse a DID to extract the method and identifier
|
|
260
|
+
*/
|
|
261
|
+
declare function parseDID(did: string): {
|
|
262
|
+
method: string;
|
|
263
|
+
identifier: string;
|
|
264
|
+
} | null;
|
|
265
|
+
/**
|
|
266
|
+
* Check if a DID is valid
|
|
267
|
+
*/
|
|
268
|
+
declare function isValidDID(did: string): boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Check if a location is still considered fresh
|
|
271
|
+
*/
|
|
272
|
+
declare function isLocationFresh(location: PeerLocation, maxAge?: number): boolean;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Query federation types for distributed queries across peers
|
|
276
|
+
*/
|
|
277
|
+
/**
|
|
278
|
+
* Generic query type (implementation-specific)
|
|
279
|
+
*/
|
|
280
|
+
interface Query {
|
|
281
|
+
type: string;
|
|
282
|
+
filters?: Record<string, unknown>;
|
|
283
|
+
limit?: number;
|
|
284
|
+
offset?: number;
|
|
285
|
+
orderBy?: string;
|
|
286
|
+
orderDirection?: 'asc' | 'desc';
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* A data source that can answer queries
|
|
290
|
+
*/
|
|
291
|
+
interface DataSource {
|
|
292
|
+
type: 'local' | 'peer' | 'cluster';
|
|
293
|
+
id: string;
|
|
294
|
+
estimatedLatency: number;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* A subquery to be executed on a specific source
|
|
298
|
+
*/
|
|
299
|
+
interface SubQuery {
|
|
300
|
+
source: DataSource;
|
|
301
|
+
query: Query;
|
|
302
|
+
estimatedCost: number;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* A plan for executing a federated query
|
|
306
|
+
*/
|
|
307
|
+
interface QueryPlan {
|
|
308
|
+
subqueries: SubQuery[];
|
|
309
|
+
aggregation: 'union' | 'join' | 'custom';
|
|
310
|
+
customAggregator?: (results: unknown[][]) => unknown[];
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Interface for routing queries to appropriate sources
|
|
314
|
+
*/
|
|
315
|
+
interface QueryRouter {
|
|
316
|
+
/** Find which sources have relevant data */
|
|
317
|
+
findSources(query: Query): Promise<DataSource[]>;
|
|
318
|
+
/** Route query to source */
|
|
319
|
+
route(query: Query, source: DataSource): Promise<unknown[]>;
|
|
320
|
+
/** Aggregate results from multiple sources */
|
|
321
|
+
aggregate(plan: QueryPlan, results: unknown[][]): unknown[];
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Wire protocol request for federated queries
|
|
325
|
+
*/
|
|
326
|
+
interface QueryRequest {
|
|
327
|
+
queryId: string;
|
|
328
|
+
query: Query;
|
|
329
|
+
auth: string;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Wire protocol response for federated queries
|
|
333
|
+
*/
|
|
334
|
+
interface QueryResponse {
|
|
335
|
+
queryId: string;
|
|
336
|
+
results: unknown[];
|
|
337
|
+
hasMore: boolean;
|
|
338
|
+
cursor?: string;
|
|
339
|
+
error?: string;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Streaming query options
|
|
343
|
+
*/
|
|
344
|
+
interface StreamingQueryOptions {
|
|
345
|
+
batchSize: number;
|
|
346
|
+
timeout: number;
|
|
347
|
+
maxResults: number;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Default streaming options
|
|
351
|
+
*/
|
|
352
|
+
declare const DEFAULT_STREAMING_OPTIONS: StreamingQueryOptions;
|
|
353
|
+
/**
|
|
354
|
+
* Estimate query cost based on filters and limits
|
|
355
|
+
*/
|
|
356
|
+
declare function estimateQueryCost(query: Query): number;
|
|
357
|
+
/**
|
|
358
|
+
* Simple union aggregation
|
|
359
|
+
*/
|
|
360
|
+
declare function unionAggregate<T>(results: T[][]): T[];
|
|
361
|
+
/**
|
|
362
|
+
* Deduplicated union aggregation (requires items to have id field)
|
|
363
|
+
*/
|
|
364
|
+
declare function deduplicatedUnion<T extends {
|
|
365
|
+
id: string;
|
|
366
|
+
}>(results: T[][]): T[];
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Permission and authorization types for xNet
|
|
370
|
+
*/
|
|
371
|
+
/**
|
|
372
|
+
* A group of users
|
|
373
|
+
*/
|
|
374
|
+
interface Group {
|
|
375
|
+
id: string;
|
|
376
|
+
members: string[];
|
|
377
|
+
memberGroups: string[];
|
|
378
|
+
managedBy: string[];
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* A role with associated capabilities
|
|
382
|
+
*/
|
|
383
|
+
interface Role {
|
|
384
|
+
id: string;
|
|
385
|
+
capabilities: Capability[];
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Available capabilities
|
|
389
|
+
*/
|
|
390
|
+
type Capability = 'read' | 'write' | 'delete' | 'share' | 'admin';
|
|
391
|
+
/**
|
|
392
|
+
* All capabilities in order of privilege
|
|
393
|
+
*/
|
|
394
|
+
declare const ALL_CAPABILITIES: Capability[];
|
|
395
|
+
/**
|
|
396
|
+
* A grant of a role to a principal
|
|
397
|
+
*/
|
|
398
|
+
interface PermissionGrant {
|
|
399
|
+
principal: string;
|
|
400
|
+
role: string;
|
|
401
|
+
scope: ResourceScope;
|
|
402
|
+
conditions?: Condition[];
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Scope of a permission
|
|
406
|
+
*/
|
|
407
|
+
interface ResourceScope {
|
|
408
|
+
type: 'workspace' | 'document' | 'block';
|
|
409
|
+
id: string;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Conditional access restriction
|
|
413
|
+
*/
|
|
414
|
+
interface Condition {
|
|
415
|
+
type: 'time' | 'ip' | 'device';
|
|
416
|
+
value: unknown;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Time-based condition
|
|
420
|
+
*/
|
|
421
|
+
interface TimeCondition extends Condition {
|
|
422
|
+
type: 'time';
|
|
423
|
+
value: {
|
|
424
|
+
after?: number;
|
|
425
|
+
before?: number;
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* IP-based condition
|
|
430
|
+
*/
|
|
431
|
+
interface IPCondition extends Condition {
|
|
432
|
+
type: 'ip';
|
|
433
|
+
value: {
|
|
434
|
+
allowList?: string[];
|
|
435
|
+
denyList?: string[];
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Interface for evaluating permissions
|
|
440
|
+
*/
|
|
441
|
+
interface PermissionEvaluator {
|
|
442
|
+
/** Check if DID has capability on resource */
|
|
443
|
+
hasCapability(did: string, capability: Capability, resource: ResourceScope): Promise<boolean>;
|
|
444
|
+
/** Resolve group membership (including nested) */
|
|
445
|
+
resolveGroups(did: string): Promise<string[]>;
|
|
446
|
+
/** Get effective permissions for DID */
|
|
447
|
+
getPermissions(did: string, resource: ResourceScope): Promise<Capability[]>;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Standard roles
|
|
451
|
+
*/
|
|
452
|
+
declare const STANDARD_ROLES: Record<string, Role>;
|
|
453
|
+
/**
|
|
454
|
+
* Check if a capability is included in a role
|
|
455
|
+
*/
|
|
456
|
+
declare function roleHasCapability(role: Role, capability: Capability): boolean;
|
|
457
|
+
/**
|
|
458
|
+
* Check if a condition is currently satisfied
|
|
459
|
+
*/
|
|
460
|
+
declare function evaluateCondition(condition: Condition, context: {
|
|
461
|
+
now?: number;
|
|
462
|
+
}): boolean;
|
|
463
|
+
/**
|
|
464
|
+
* Get the most permissive capability from a list
|
|
465
|
+
*/
|
|
466
|
+
declare function getMostPermissiveCapability(capabilities: Capability[]): Capability | null;
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Authorization types for xNet's encryption-first authorization system.
|
|
470
|
+
*
|
|
471
|
+
* These types form the foundation of the authorization model where
|
|
472
|
+
* "the ability to decrypt" IS access control.
|
|
473
|
+
*/
|
|
474
|
+
/**
|
|
475
|
+
* DID - Decentralized Identifier for user identity.
|
|
476
|
+
* Redeclared here to avoid circular imports.
|
|
477
|
+
*/
|
|
478
|
+
type DID$1 = `did:key:${string}`;
|
|
479
|
+
/**
|
|
480
|
+
* Schema IRI - globally unique identifier for a schema.
|
|
481
|
+
*/
|
|
482
|
+
type SchemaIRI = `xnet://${string}/${string}`;
|
|
483
|
+
/**
|
|
484
|
+
* Canonical authorization actions.
|
|
485
|
+
* All action checks map to one of these values.
|
|
486
|
+
*/
|
|
487
|
+
declare const AUTH_ACTIONS: readonly ["read", "write", "delete", "share", "admin"];
|
|
488
|
+
/**
|
|
489
|
+
* An authorization action that can be checked or granted.
|
|
490
|
+
*/
|
|
491
|
+
type AuthAction = (typeof AUTH_ACTIONS)[number];
|
|
492
|
+
/**
|
|
493
|
+
* The result of an authorization check.
|
|
494
|
+
* Contains whether access is allowed plus diagnostic info for debugging.
|
|
495
|
+
*/
|
|
496
|
+
interface AuthDecision {
|
|
497
|
+
/** Whether the action is permitted */
|
|
498
|
+
allowed: boolean;
|
|
499
|
+
/** The action that was checked */
|
|
500
|
+
action: AuthAction;
|
|
501
|
+
/** The DID of the subject requesting access */
|
|
502
|
+
subject: DID$1;
|
|
503
|
+
/** The resource (node ID) being accessed */
|
|
504
|
+
resource: string;
|
|
505
|
+
/** Roles the subject holds that contributed to the decision */
|
|
506
|
+
roles: string[];
|
|
507
|
+
/** Grant IDs that contributed to allowing access */
|
|
508
|
+
grants: string[];
|
|
509
|
+
/** If denied, the reasons why */
|
|
510
|
+
reasons: AuthDenyReason[];
|
|
511
|
+
/** Whether this result came from cache */
|
|
512
|
+
cached: boolean;
|
|
513
|
+
/** Timestamp when the decision was evaluated */
|
|
514
|
+
evaluatedAt: number;
|
|
515
|
+
/** How long the evaluation took in milliseconds */
|
|
516
|
+
duration: number;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Reason codes for authorization denial.
|
|
520
|
+
* Used for debugging and user-friendly error messages.
|
|
521
|
+
*/
|
|
522
|
+
type AuthDenyReason = 'DENY_NODE_POLICY' | 'DENY_NO_ROLE_MATCH' | 'DENY_NO_GRANT' | 'DENY_UCAN_INVALID' | 'DENY_UCAN_REVOKED' | 'DENY_UCAN_EXPIRED' | 'DENY_DEPTH_EXCEEDED' | 'DENY_NOT_AUTHENTICATED' | 'DENY_FIELD_RESTRICTED' | 'DENY_GRANT_EXPIRED' | 'DENY_STALE_OFFLINE';
|
|
523
|
+
/**
|
|
524
|
+
* Extended decision with step-by-step evaluation trace.
|
|
525
|
+
* Used by the explain() API for debugging and AI agent validation.
|
|
526
|
+
*/
|
|
527
|
+
interface AuthTrace extends AuthDecision {
|
|
528
|
+
/** Each step in the evaluation pipeline */
|
|
529
|
+
steps: AuthTraceStep[];
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* A single step in the authorization evaluation pipeline.
|
|
533
|
+
*/
|
|
534
|
+
interface AuthTraceStep {
|
|
535
|
+
/** Which phase of evaluation this step represents */
|
|
536
|
+
phase: 'node-deny' | 'role-resolve' | 'schema-eval' | 'grant-check' | 'public-check';
|
|
537
|
+
/** Inputs to this evaluation step */
|
|
538
|
+
input: Record<string, unknown>;
|
|
539
|
+
/** Outputs from this evaluation step */
|
|
540
|
+
output: Record<string, unknown>;
|
|
541
|
+
/** How long this step took in milliseconds */
|
|
542
|
+
duration: number;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Authorization rules defined in a schema.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```typescript
|
|
549
|
+
* authorization: {
|
|
550
|
+
* roles: {
|
|
551
|
+
* owner: role.creator(),
|
|
552
|
+
* editor: role.property('editors'),
|
|
553
|
+
* admin: role.relation('project', 'admin')
|
|
554
|
+
* },
|
|
555
|
+
* actions: {
|
|
556
|
+
* read: allow('editor', 'admin', 'owner'),
|
|
557
|
+
* write: allow('editor', 'admin', 'owner'),
|
|
558
|
+
* delete: allow('admin', 'owner'),
|
|
559
|
+
* share: allow('admin', 'owner')
|
|
560
|
+
* },
|
|
561
|
+
* publicProps: ['title']
|
|
562
|
+
* }
|
|
563
|
+
* ```
|
|
564
|
+
*/
|
|
565
|
+
interface AuthorizationDefinition<TActions extends Record<string, AuthExpression> = Record<string, AuthExpression>, TRoles extends Record<string, RoleResolver> = Record<string, RoleResolver>> {
|
|
566
|
+
/** Role definitions - how to determine if a user holds each role */
|
|
567
|
+
roles: TRoles;
|
|
568
|
+
/** Action expressions - which roles can perform each action */
|
|
569
|
+
actions: TActions;
|
|
570
|
+
/** Properties that are publicly readable even for private nodes */
|
|
571
|
+
publicProps?: string[];
|
|
572
|
+
/** Field-level access rules */
|
|
573
|
+
fieldRules?: Record<string, {
|
|
574
|
+
allow: AuthExpression;
|
|
575
|
+
deny?: AuthExpression;
|
|
576
|
+
}>;
|
|
577
|
+
/** How node-level policy interacts with schema policy */
|
|
578
|
+
nodePolicy?: {
|
|
579
|
+
mode: 'extend';
|
|
580
|
+
allow: ('deny' | 'fieldRules' | 'conditions')[];
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Serialized form of AuthorizationDefinition for storage in Schema.
|
|
585
|
+
*/
|
|
586
|
+
interface SerializedAuthorization {
|
|
587
|
+
roles: Record<string, SerializedRoleResolver>;
|
|
588
|
+
actions: Record<string, SerializedAuthExpression>;
|
|
589
|
+
publicProps?: string[];
|
|
590
|
+
fieldRules?: Record<string, {
|
|
591
|
+
allow: SerializedAuthExpression;
|
|
592
|
+
deny?: SerializedAuthExpression;
|
|
593
|
+
}>;
|
|
594
|
+
nodePolicy?: {
|
|
595
|
+
mode: 'extend';
|
|
596
|
+
allow: string[];
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Extract action keys from an AuthorizationDefinition.
|
|
601
|
+
*/
|
|
602
|
+
type ActionKey<TAuth extends AuthorizationDefinition> = keyof TAuth['actions'] & string;
|
|
603
|
+
/**
|
|
604
|
+
* Extract role keys from an AuthorizationDefinition.
|
|
605
|
+
*/
|
|
606
|
+
type RoleKey<TAuth extends AuthorizationDefinition> = keyof TAuth['roles'] & string;
|
|
607
|
+
/**
|
|
608
|
+
* Extract the action type from a schema with authorization.
|
|
609
|
+
*/
|
|
610
|
+
type SchemaAction<S extends {
|
|
611
|
+
authorization: AuthorizationDefinition;
|
|
612
|
+
}> = ActionKey<S['authorization']>;
|
|
613
|
+
/**
|
|
614
|
+
* Authorization expression AST node.
|
|
615
|
+
* Evaluated against a set of roles to determine access.
|
|
616
|
+
*/
|
|
617
|
+
type AuthExpression = AllowExpr | DenyExpr | AndExpr | OrExpr | NotExpr | RoleRefExpr | PublicExpr | AuthenticatedExpr;
|
|
618
|
+
/**
|
|
619
|
+
* Serialized form of AuthExpression for JSON storage.
|
|
620
|
+
*/
|
|
621
|
+
type SerializedAuthExpression = {
|
|
622
|
+
_tag: 'allow';
|
|
623
|
+
roles: string[];
|
|
624
|
+
} | {
|
|
625
|
+
_tag: 'deny';
|
|
626
|
+
roles: string[];
|
|
627
|
+
} | {
|
|
628
|
+
_tag: 'and';
|
|
629
|
+
exprs: SerializedAuthExpression[];
|
|
630
|
+
} | {
|
|
631
|
+
_tag: 'or';
|
|
632
|
+
exprs: SerializedAuthExpression[];
|
|
633
|
+
} | {
|
|
634
|
+
_tag: 'not';
|
|
635
|
+
expr: SerializedAuthExpression;
|
|
636
|
+
} | {
|
|
637
|
+
_tag: 'roleRef';
|
|
638
|
+
role: string;
|
|
639
|
+
} | {
|
|
640
|
+
_tag: 'public';
|
|
641
|
+
} | {
|
|
642
|
+
_tag: 'authenticated';
|
|
643
|
+
};
|
|
644
|
+
/**
|
|
645
|
+
* Allow access if the subject has any of the specified roles.
|
|
646
|
+
*/
|
|
647
|
+
interface AllowExpr {
|
|
648
|
+
readonly _tag: 'allow';
|
|
649
|
+
readonly roles: readonly string[];
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Deny access if the subject has any of the specified roles.
|
|
653
|
+
* Deny always takes precedence over allow.
|
|
654
|
+
*/
|
|
655
|
+
interface DenyExpr {
|
|
656
|
+
readonly _tag: 'deny';
|
|
657
|
+
readonly roles: readonly string[];
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Logical AND - all sub-expressions must be true.
|
|
661
|
+
*/
|
|
662
|
+
interface AndExpr {
|
|
663
|
+
readonly _tag: 'and';
|
|
664
|
+
readonly exprs: readonly AuthExpression[];
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Logical OR - any sub-expression must be true.
|
|
668
|
+
*/
|
|
669
|
+
interface OrExpr {
|
|
670
|
+
readonly _tag: 'or';
|
|
671
|
+
readonly exprs: readonly AuthExpression[];
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Logical NOT - negate the sub-expression.
|
|
675
|
+
*/
|
|
676
|
+
interface NotExpr {
|
|
677
|
+
readonly _tag: 'not';
|
|
678
|
+
readonly expr: AuthExpression;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Reference to a named role.
|
|
682
|
+
*/
|
|
683
|
+
interface RoleRefExpr {
|
|
684
|
+
readonly _tag: 'roleRef';
|
|
685
|
+
readonly role: string;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Public access - always allows access.
|
|
689
|
+
*/
|
|
690
|
+
interface PublicExpr {
|
|
691
|
+
readonly _tag: 'public';
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Authenticated access - allows any authenticated user.
|
|
695
|
+
*/
|
|
696
|
+
interface AuthenticatedExpr {
|
|
697
|
+
readonly _tag: 'authenticated';
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* How to determine if a user holds a role.
|
|
701
|
+
*/
|
|
702
|
+
type RoleResolver = CreatorRoleResolver | PropertyRoleResolver | RelationRoleResolver;
|
|
703
|
+
/**
|
|
704
|
+
* Serialized form of RoleResolver for JSON storage.
|
|
705
|
+
*/
|
|
706
|
+
type SerializedRoleResolver = {
|
|
707
|
+
_tag: 'creator';
|
|
708
|
+
} | {
|
|
709
|
+
_tag: 'property';
|
|
710
|
+
propertyName: string;
|
|
711
|
+
} | {
|
|
712
|
+
_tag: 'relation';
|
|
713
|
+
relationName: string;
|
|
714
|
+
targetRole: string;
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* Role held by the node's creator.
|
|
718
|
+
*/
|
|
719
|
+
interface CreatorRoleResolver {
|
|
720
|
+
readonly _tag: 'creator';
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Role determined by a person property on the node.
|
|
724
|
+
* The DID(s) in that property hold this role.
|
|
725
|
+
*/
|
|
726
|
+
interface PropertyRoleResolver {
|
|
727
|
+
readonly _tag: 'property';
|
|
728
|
+
readonly propertyName: string;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Role inherited from a related node.
|
|
732
|
+
* Users who hold `targetRole` on the related node hold this role.
|
|
733
|
+
*/
|
|
734
|
+
interface RelationRoleResolver {
|
|
735
|
+
readonly _tag: 'relation';
|
|
736
|
+
readonly relationName: string;
|
|
737
|
+
readonly targetRole: string;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Input for an authorization check.
|
|
741
|
+
*/
|
|
742
|
+
interface AuthCheckInput {
|
|
743
|
+
/** The DID of the subject requesting access */
|
|
744
|
+
subject: DID$1;
|
|
745
|
+
/** The action being requested */
|
|
746
|
+
action: AuthAction;
|
|
747
|
+
/** The node being accessed */
|
|
748
|
+
nodeId: string;
|
|
749
|
+
/** Pre-loaded node to avoid re-fetching (optional) */
|
|
750
|
+
node?: {
|
|
751
|
+
schemaId: SchemaIRI;
|
|
752
|
+
createdBy: DID$1;
|
|
753
|
+
properties?: Record<string, unknown>;
|
|
754
|
+
};
|
|
755
|
+
/** Patch for field-level checks on update (optional) */
|
|
756
|
+
patch?: Record<string, unknown>;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Interface for evaluating authorization policies.
|
|
760
|
+
* Supersedes the older PermissionEvaluator interface.
|
|
761
|
+
*/
|
|
762
|
+
interface PolicyEvaluator {
|
|
763
|
+
/** Check if subject can perform action on resource */
|
|
764
|
+
can(input: AuthCheckInput): Promise<AuthDecision>;
|
|
765
|
+
/** Check with full trace for debugging */
|
|
766
|
+
explain(input: AuthCheckInput): Promise<AuthTrace>;
|
|
767
|
+
/** Invalidate cached decisions for a resource */
|
|
768
|
+
invalidate(nodeId: string): void;
|
|
769
|
+
/** Invalidate all cached decisions for a subject */
|
|
770
|
+
invalidateSubject(did: DID$1): void;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* @xnetjs/core - Core types, schemas, and content addressing
|
|
775
|
+
*/
|
|
776
|
+
|
|
777
|
+
type DID = `did:key:${string}`;
|
|
778
|
+
type DocumentPath = `xnet://${DID}/workspace/${string}/doc/${string}`;
|
|
779
|
+
|
|
780
|
+
export { ALL_CAPABILITIES, AUTH_ACTIONS, type ActionKey, type AllowExpr, type AndExpr, type AuthAction, type AuthCheckInput, type AuthDecision, type AuthDenyReason, type AuthExpression, type AuthTrace, type AuthTraceStep, type AuthenticatedExpr, type AuthorizationDefinition, BOOTSTRAP_PEERS, type Capability, type ChainStatus, type Condition, type ContentChunk, type ContentId, type ContentResolver, type ContentTree, type CreatorRoleResolver, DEFAULT_SNAPSHOT_TRIGGERS, DEFAULT_STREAMING_OPTIONS, DHT_CONFIG, type DID, type DIDResolution, type DIDResolver, type DataSource, type DenyExpr, type DocumentLoad, type DocumentPath, type Fork, type Group, type IPCondition, type MerkleNode, type NotExpr, type OrExpr, type PeerLocation, type PermissionEvaluator, type PermissionGrant, type PolicyEvaluator, type PropertyRoleResolver, type PublicExpr, type Query, type QueryPlan, type QueryRequest, type QueryResponse, type QueryRouter, RESOLUTION_CACHE_CONFIG, type RelationRoleResolver, type ResolutionStrategy, type ResourceScope, type Role, type RoleKey, type RoleRefExpr, type RoleResolver, STANDARD_ROLES, type SchemaAction, type SerializedAuthExpression, type SerializedAuthorization, type SerializedRoleResolver, type SignedUpdate, type Snapshot, type SnapshotTriggers, type StreamingQueryOptions, type SubQuery, type TimeCondition, type UpdateVerifier, type VectorClock, buildMerkleTree, compareVectorClocks, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, getMostPermissiveCapability, hashContent, incrementVectorClock, isLocationFresh, isValidDID, isValidProgression, mergeStateVectors, mergeVectorClocks, parseContentId, parseDID, roleHasCapability, shouldCreateSnapshot, unionAggregate, verifyContent, verifyUpdateChain };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// src/hashing.ts
|
|
2
|
+
import { blake3 } from "@noble/hashes/blake3.js";
|
|
3
|
+
function hashContent(data) {
|
|
4
|
+
const hash = blake3(data);
|
|
5
|
+
return bytesToHex(hash);
|
|
6
|
+
}
|
|
7
|
+
function createContentId(hash) {
|
|
8
|
+
return `cid:blake3:${hash}`;
|
|
9
|
+
}
|
|
10
|
+
function parseContentId(cid) {
|
|
11
|
+
const match = cid.match(/^cid:blake3:([a-f0-9]+)$/);
|
|
12
|
+
if (!match) throw new Error(`Invalid CID: ${cid}`);
|
|
13
|
+
return match[1];
|
|
14
|
+
}
|
|
15
|
+
function verifyContent(cid, data) {
|
|
16
|
+
const expectedHash = parseContentId(cid);
|
|
17
|
+
const actualHash = hashContent(data);
|
|
18
|
+
return expectedHash === actualHash;
|
|
19
|
+
}
|
|
20
|
+
function createChunk(data) {
|
|
21
|
+
return {
|
|
22
|
+
data,
|
|
23
|
+
hash: hashContent(data),
|
|
24
|
+
size: data.length
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function buildMerkleTree(chunks) {
|
|
28
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
29
|
+
if (chunks.length === 0) {
|
|
30
|
+
const emptyHash = hashContent(new Uint8Array(0));
|
|
31
|
+
nodes.set(emptyHash, { hash: emptyHash, data: new Uint8Array(0) });
|
|
32
|
+
return { rootHash: emptyHash, nodes };
|
|
33
|
+
}
|
|
34
|
+
const leafHashes = [];
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
nodes.set(chunk.hash, {
|
|
37
|
+
hash: chunk.hash,
|
|
38
|
+
data: chunk.data
|
|
39
|
+
});
|
|
40
|
+
leafHashes.push(chunk.hash);
|
|
41
|
+
}
|
|
42
|
+
let currentLevel = leafHashes;
|
|
43
|
+
while (currentLevel.length > 1) {
|
|
44
|
+
const nextLevel = [];
|
|
45
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
46
|
+
const left = currentLevel[i];
|
|
47
|
+
const right = currentLevel[i + 1] || left;
|
|
48
|
+
const combined = new TextEncoder().encode(left + right);
|
|
49
|
+
const parentHash = hashContent(combined);
|
|
50
|
+
nodes.set(parentHash, {
|
|
51
|
+
hash: parentHash,
|
|
52
|
+
children: left === right ? [left] : [left, right]
|
|
53
|
+
});
|
|
54
|
+
nextLevel.push(parentHash);
|
|
55
|
+
}
|
|
56
|
+
currentLevel = nextLevel;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
rootHash: currentLevel[0],
|
|
60
|
+
nodes
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function bytesToHex(bytes) {
|
|
64
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/snapshots.ts
|
|
68
|
+
var DEFAULT_SNAPSHOT_TRIGGERS = {
|
|
69
|
+
updateCount: 1e4,
|
|
70
|
+
timeInterval: 24 * 60 * 60 * 1e3,
|
|
71
|
+
// 24 hours
|
|
72
|
+
storagePressure: 0.8
|
|
73
|
+
// 80%
|
|
74
|
+
};
|
|
75
|
+
function shouldCreateSnapshot(updateCount, lastSnapshotTime, storageUsed, storageTotal, triggers = DEFAULT_SNAPSHOT_TRIGGERS) {
|
|
76
|
+
if (updateCount >= triggers.updateCount) return true;
|
|
77
|
+
if (Date.now() - lastSnapshotTime >= triggers.timeInterval) return true;
|
|
78
|
+
if (storageTotal > 0 && storageUsed / storageTotal >= triggers.storagePressure) return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function mergeStateVectors(base, _updates) {
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/updates.ts
|
|
86
|
+
function compareVectorClocks(a, b) {
|
|
87
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
88
|
+
let aGreater = false;
|
|
89
|
+
let bGreater = false;
|
|
90
|
+
for (const key of allKeys) {
|
|
91
|
+
const aVal = a[key] || 0;
|
|
92
|
+
const bVal = b[key] || 0;
|
|
93
|
+
if (aVal > bVal) aGreater = true;
|
|
94
|
+
if (bVal > aVal) bGreater = true;
|
|
95
|
+
}
|
|
96
|
+
if (aGreater && !bGreater) return 1;
|
|
97
|
+
if (bGreater && !aGreater) return -1;
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
function isValidProgression(prev, next, authorId) {
|
|
101
|
+
const prevAuthor = prev[authorId] || 0;
|
|
102
|
+
const nextAuthor = next[authorId] || 0;
|
|
103
|
+
if (nextAuthor !== prevAuthor + 1) return false;
|
|
104
|
+
for (const key of Object.keys(prev)) {
|
|
105
|
+
if (key !== authorId) {
|
|
106
|
+
const prevVal = prev[key] || 0;
|
|
107
|
+
const nextVal = next[key] || 0;
|
|
108
|
+
if (nextVal < prevVal) return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function mergeVectorClocks(a, b) {
|
|
114
|
+
const result = { ...a };
|
|
115
|
+
for (const [key, value] of Object.entries(b)) {
|
|
116
|
+
result[key] = Math.max(result[key] || 0, value);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function incrementVectorClock(clock, peerId) {
|
|
121
|
+
return {
|
|
122
|
+
...clock,
|
|
123
|
+
[peerId]: (clock[peerId] || 0) + 1
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/verification.ts
|
|
128
|
+
function detectFork(updates) {
|
|
129
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const update of updates) {
|
|
131
|
+
const existing = byParent.get(update.parentHash) || [];
|
|
132
|
+
existing.push(update);
|
|
133
|
+
byParent.set(update.parentHash, existing);
|
|
134
|
+
}
|
|
135
|
+
for (const [parentHash, children] of byParent) {
|
|
136
|
+
if (children.length > 1) {
|
|
137
|
+
const branch1 = buildBranch(children[0], updates);
|
|
138
|
+
const branch2 = buildBranch(children[1], updates);
|
|
139
|
+
return {
|
|
140
|
+
commonAncestor: parentHash,
|
|
141
|
+
branch1,
|
|
142
|
+
branch2
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function buildBranch(start, allUpdates) {
|
|
149
|
+
const branch = [start];
|
|
150
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
151
|
+
for (const update of allUpdates) {
|
|
152
|
+
byParent.set(update.parentHash, update);
|
|
153
|
+
}
|
|
154
|
+
let current = start;
|
|
155
|
+
while (true) {
|
|
156
|
+
const next = byParent.get(current.updateHash);
|
|
157
|
+
if (!next) break;
|
|
158
|
+
branch.push(next);
|
|
159
|
+
current = next;
|
|
160
|
+
}
|
|
161
|
+
return branch;
|
|
162
|
+
}
|
|
163
|
+
async function verifyUpdateChain(updates, getPublicKey, verifySignature) {
|
|
164
|
+
const errors = [];
|
|
165
|
+
const forks = [];
|
|
166
|
+
const sorted = [...updates].sort((a, b) => a.timestamp - b.timestamp);
|
|
167
|
+
const fork = detectFork(sorted);
|
|
168
|
+
if (fork) {
|
|
169
|
+
forks.push(fork);
|
|
170
|
+
errors.push(`Fork detected at ${fork.commonAncestor}`);
|
|
171
|
+
}
|
|
172
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
173
|
+
const update = sorted[i];
|
|
174
|
+
let publicKey;
|
|
175
|
+
try {
|
|
176
|
+
publicKey = await getPublicKey(update.authorDID);
|
|
177
|
+
} catch {
|
|
178
|
+
errors.push(`Failed to get public key for ${update.authorDID}`);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const signatureData = new Uint8Array([
|
|
182
|
+
...update.update,
|
|
183
|
+
...new TextEncoder().encode(update.parentHash),
|
|
184
|
+
...new TextEncoder().encode(update.authorDID),
|
|
185
|
+
...new TextEncoder().encode(update.timestamp.toString())
|
|
186
|
+
]);
|
|
187
|
+
if (!verifySignature(signatureData, update.signature, publicKey)) {
|
|
188
|
+
errors.push(`Invalid signature for update ${update.updateHash}`);
|
|
189
|
+
}
|
|
190
|
+
if (i > 0) {
|
|
191
|
+
const prev = sorted[i - 1];
|
|
192
|
+
if (update.parentHash === prev.updateHash) {
|
|
193
|
+
if (!isValidProgression(prev.vectorClock, update.vectorClock, update.authorDID)) {
|
|
194
|
+
errors.push(`Invalid vector clock progression at ${update.updateHash}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
valid: errors.length === 0,
|
|
201
|
+
errors,
|
|
202
|
+
forks
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/resolution.ts
|
|
207
|
+
var BOOTSTRAP_PEERS = [
|
|
208
|
+
"/dns4/bootstrap1.xnet.io/tcp/4001/p2p/12D3KooWBootstrap1",
|
|
209
|
+
"/dns4/bootstrap2.xnet.io/tcp/4001/p2p/12D3KooWBootstrap2"
|
|
210
|
+
// Real peers added at deployment
|
|
211
|
+
];
|
|
212
|
+
var DHT_CONFIG = {
|
|
213
|
+
protocol: "/xnet/kad/1.0.0",
|
|
214
|
+
replicationFactor: 20,
|
|
215
|
+
refreshInterval: 60 * 60 * 1e3
|
|
216
|
+
// 1 hour
|
|
217
|
+
};
|
|
218
|
+
var RESOLUTION_CACHE_CONFIG = {
|
|
219
|
+
maxEntries: 1e3,
|
|
220
|
+
ttl: 5 * 60 * 1e3,
|
|
221
|
+
// 5 minutes
|
|
222
|
+
staleWhileRevalidate: 60 * 60 * 1e3
|
|
223
|
+
// 1 hour
|
|
224
|
+
};
|
|
225
|
+
function parseDID(did) {
|
|
226
|
+
const match = did.match(/^did:([a-z]+):(.+)$/);
|
|
227
|
+
if (!match) return null;
|
|
228
|
+
return { method: match[1], identifier: match[2] };
|
|
229
|
+
}
|
|
230
|
+
function isValidDID(did) {
|
|
231
|
+
return parseDID(did) !== null;
|
|
232
|
+
}
|
|
233
|
+
function isLocationFresh(location, maxAge = 5 * 60 * 1e3) {
|
|
234
|
+
return Date.now() - location.lastSeen < maxAge;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/federation.ts
|
|
238
|
+
var DEFAULT_STREAMING_OPTIONS = {
|
|
239
|
+
batchSize: 100,
|
|
240
|
+
timeout: 3e4,
|
|
241
|
+
// 30 seconds
|
|
242
|
+
maxResults: 1e4
|
|
243
|
+
};
|
|
244
|
+
function estimateQueryCost(query) {
|
|
245
|
+
let cost = 1;
|
|
246
|
+
const filterCount = Object.keys(query.filters || {}).length;
|
|
247
|
+
cost *= Math.max(0.1, 1 - filterCount * 0.1);
|
|
248
|
+
if (query.limit) {
|
|
249
|
+
cost *= Math.min(1, query.limit / 1e3);
|
|
250
|
+
}
|
|
251
|
+
return cost;
|
|
252
|
+
}
|
|
253
|
+
function unionAggregate(results) {
|
|
254
|
+
return results.flat();
|
|
255
|
+
}
|
|
256
|
+
function deduplicatedUnion(results) {
|
|
257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
258
|
+
const output = [];
|
|
259
|
+
for (const batch of results) {
|
|
260
|
+
for (const item of batch) {
|
|
261
|
+
if (!seen.has(item.id)) {
|
|
262
|
+
seen.add(item.id);
|
|
263
|
+
output.push(item);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return output;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/permissions.ts
|
|
271
|
+
var ALL_CAPABILITIES = ["read", "write", "delete", "share", "admin"];
|
|
272
|
+
var STANDARD_ROLES = {
|
|
273
|
+
viewer: {
|
|
274
|
+
id: "viewer",
|
|
275
|
+
capabilities: ["read"]
|
|
276
|
+
},
|
|
277
|
+
editor: {
|
|
278
|
+
id: "editor",
|
|
279
|
+
capabilities: ["read", "write"]
|
|
280
|
+
},
|
|
281
|
+
admin: {
|
|
282
|
+
id: "admin",
|
|
283
|
+
capabilities: ["read", "write", "delete", "share", "admin"]
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
function roleHasCapability(role, capability) {
|
|
287
|
+
return role.capabilities.includes(capability);
|
|
288
|
+
}
|
|
289
|
+
function evaluateCondition(condition, context) {
|
|
290
|
+
switch (condition.type) {
|
|
291
|
+
case "time": {
|
|
292
|
+
const timeCondition = condition;
|
|
293
|
+
const now = context.now || Date.now();
|
|
294
|
+
if (timeCondition.value.after && now < timeCondition.value.after) return false;
|
|
295
|
+
if (timeCondition.value.before && now > timeCondition.value.before) return false;
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function getMostPermissiveCapability(capabilities) {
|
|
303
|
+
for (const cap of [...ALL_CAPABILITIES].reverse()) {
|
|
304
|
+
if (capabilities.includes(cap)) return cap;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/auth-types.ts
|
|
310
|
+
var AUTH_ACTIONS = ["read", "write", "delete", "share", "admin"];
|
|
311
|
+
export {
|
|
312
|
+
ALL_CAPABILITIES,
|
|
313
|
+
AUTH_ACTIONS,
|
|
314
|
+
BOOTSTRAP_PEERS,
|
|
315
|
+
DEFAULT_SNAPSHOT_TRIGGERS,
|
|
316
|
+
DEFAULT_STREAMING_OPTIONS,
|
|
317
|
+
DHT_CONFIG,
|
|
318
|
+
RESOLUTION_CACHE_CONFIG,
|
|
319
|
+
STANDARD_ROLES,
|
|
320
|
+
buildMerkleTree,
|
|
321
|
+
compareVectorClocks,
|
|
322
|
+
createChunk,
|
|
323
|
+
createContentId,
|
|
324
|
+
deduplicatedUnion,
|
|
325
|
+
detectFork,
|
|
326
|
+
estimateQueryCost,
|
|
327
|
+
evaluateCondition,
|
|
328
|
+
getMostPermissiveCapability,
|
|
329
|
+
hashContent,
|
|
330
|
+
incrementVectorClock,
|
|
331
|
+
isLocationFresh,
|
|
332
|
+
isValidDID,
|
|
333
|
+
isValidProgression,
|
|
334
|
+
mergeStateVectors,
|
|
335
|
+
mergeVectorClocks,
|
|
336
|
+
parseContentId,
|
|
337
|
+
parseDID,
|
|
338
|
+
roleHasCapability,
|
|
339
|
+
shouldCreateSnapshot,
|
|
340
|
+
unionAggregate,
|
|
341
|
+
verifyContent,
|
|
342
|
+
verifyUpdateChain
|
|
343
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xnetjs/core",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/crs48/xNet"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.0",
|
|
29
|
+
"typescript": "^5.4.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@noble/hashes": "^2.0.1"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"clean": "rm -rf dist"
|
|
38
|
+
}
|
|
39
|
+
}
|