@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 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
+ ```
@@ -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
+ }