@xnetjs/core 0.12.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -385,9 +385,13 @@ interface Role {
385
385
  capabilities: Capability[];
386
386
  }
387
387
  /**
388
- * Available capabilities
388
+ * Available capabilities.
389
+ *
390
+ * `create`/`update` are refinements of `write` (exploration 0304): a role
391
+ * holding `write` implicitly covers both; the granular capabilities cover
392
+ * only themselves.
389
393
  */
390
- type Capability = 'read' | 'write' | 'delete' | 'share' | 'admin';
394
+ type Capability = 'read' | 'create' | 'update' | 'write' | 'delete' | 'share' | 'admin';
391
395
  /**
392
396
  * All capabilities in order of privilege
393
397
  */
@@ -451,7 +455,8 @@ interface PermissionEvaluator {
451
455
  */
452
456
  declare const STANDARD_ROLES: Record<string, Role>;
453
457
  /**
454
- * Check if a capability is included in a role
458
+ * Check if a capability is included in a role.
459
+ * A role holding `write` covers the `create`/`update` refinements.
455
460
  */
456
461
  declare function roleHasCapability(role: Role, capability: Capability): boolean;
457
462
  /**
@@ -483,12 +488,46 @@ type SchemaIRI = `xnet://${string}/${string}`;
483
488
  /**
484
489
  * Canonical authorization actions.
485
490
  * All action checks map to one of these values.
491
+ *
492
+ * `create` and `update` are refinements of the coarse `write` action
493
+ * (exploration 0304): `create` governs bringing a node into existence,
494
+ * `update` governs mutating a node that already exists. A schema MAY declare
495
+ * either refinement; when one is absent its expression falls back to the
496
+ * schema's `write` expression (see {@link actionExpressionOrder}), so schemas
497
+ * that only declare `write` behave exactly as before. Checking `write` on an
498
+ * existing node is equivalent to checking `update`.
499
+ *
500
+ * `create` checks evaluate roles against the *draft* node built from the
501
+ * create payload (`createdBy` = the caller). Because `role.creator()` then
502
+ * always matches, a schema that wants real admission control must exclude the
503
+ * creator role from `create` and require a container role instead — the
504
+ * draft's relation properties (e.g. `space`, `channel`) resolve normally.
486
505
  */
487
- declare const AUTH_ACTIONS: readonly ["read", "write", "delete", "share", "admin"];
506
+ declare const AUTH_ACTIONS: readonly ["read", "create", "update", "write", "delete", "share", "admin"];
488
507
  /**
489
508
  * An authorization action that can be checked or granted.
490
509
  */
491
510
  type AuthAction = (typeof AUTH_ACTIONS)[number];
511
+ /**
512
+ * Expression lookup order for a checked action — the first action name whose
513
+ * expression a schema defines wins (exploration 0304 fallback table):
514
+ *
515
+ * | checked | expression used |
516
+ * |--------------------|-------------------------------------|
517
+ * | `create` | `actions.create` ?? `actions.write` |
518
+ * | `update` / `write` | `actions.update` ?? `actions.write` |
519
+ * | anything else | `actions[action]` |
520
+ */
521
+ declare function actionExpressionOrder(action: AuthAction): readonly AuthAction[];
522
+ /**
523
+ * Whether a granted action satisfies a checked action.
524
+ *
525
+ * A `write` grant covers its refinements (`create`, `update`); a granular
526
+ * grant covers only itself. A granular grant does NOT satisfy a legacy
527
+ * `write` check (fail closed — old callers checking `write` on an existing
528
+ * node mean update, which `update` covers).
529
+ */
530
+ declare function grantActionSatisfies(granted: AuthAction, checked: AuthAction): boolean;
492
531
  /**
493
532
  * The result of an authorization check.
494
533
  * Contains whether access is allowed plus diagnostic info for debugging.
@@ -554,7 +593,11 @@ interface AuthTraceStep {
554
593
  * },
555
594
  * actions: {
556
595
  * read: allow('editor', 'admin', 'owner'),
596
+ * // coarse mutation policy; also the fallback for create/update
557
597
  * write: allow('editor', 'admin', 'owner'),
598
+ * // optional refinements (0304): who may add vs. who may modify
599
+ * create: allow('editor', 'admin'),
600
+ * update: allow('owner', 'admin'),
558
601
  * delete: allow('admin', 'owner'),
559
602
  * share: allow('admin', 'owner')
560
603
  * },
@@ -865,14 +908,14 @@ declare function validateExternalUrl(rawUrl: string): {
865
908
  };
866
909
 
867
910
  /**
868
- * Retry/backoff policies (exploration 0300).
911
+ * Retry/backoff policies (exploration 0303).
869
912
  *
870
913
  * One dependency-free vocabulary for every reconnect/retry loop in the repo,
871
914
  * replacing the hand-rolled backoff math that had drifted into three parallel
872
915
  * implementations (sync connection-manager, WebSocketSyncProvider, webhook
873
916
  * emitter). Deliberately shaped like Effect's `Schedule` combinators so a
874
917
  * later migration would be mechanical — but scope-guarded: if this module
875
- * needs union/intersect/cron/hedging, stop and re-read exploration 0300.
918
+ * needs union/intersect/cron/hedging, stop and re-read exploration 0303.
876
919
  */
877
920
  /**
878
921
  * A retry policy maps a 1-based attempt number to the delay (in ms) to wait
@@ -902,7 +945,7 @@ declare function jittered(policy: RetryPolicy, ratio?: number, random?: () => nu
902
945
  declare function limitAttempts(policy: RetryPolicy, maxAttempts: number): RetryPolicy;
903
946
 
904
947
  /**
905
- * Tagged errors (exploration 0300).
948
+ * Tagged errors (exploration 0303).
906
949
  *
907
950
  * The repo-wide convention for structured errors, formalizing what
908
951
  * `NodeRelayError.code` and friends already did by hand: every structured
@@ -925,7 +968,7 @@ declare abstract class TaggedError<Tag extends string = string> extends Error {
925
968
  declare function isTagged<Tag extends string>(error: unknown, tag: Tag): error is TaggedError<Tag>;
926
969
 
927
970
  /**
928
- * Single-flight promise memoization (exploration 0300).
971
+ * Single-flight promise memoization (exploration 0303).
929
972
  *
930
973
  * The one implementation of the "share the in-flight promise so concurrent
931
974
  * callers don't convoy the backend" pattern that had been independently
@@ -954,27 +997,28 @@ interface SingleFlightOptions {
954
997
  */
955
998
  declare function singleFlight<K, V>(map: Map<K, Promise<V>>, key: K, fn: () => Promise<V>, options?: SingleFlightOptions): Promise<V>;
956
999
 
1000
+ /** Protocol version at which the grinding-resistant tiebreak key activates. */
1001
+ declare const LWW_TIEBREAK_KEY_VERSION = 4;
957
1002
  /**
958
- * The ONE Last-Write-Wins ordering for xNet (docs/specs/protocol §L1.7,
959
- * exploration 0276).
960
- *
961
- * Per-property conflict resolution and change-log application ordering were
962
- * previously re-implemented in three places (`NodeStore.applyChange`, the
963
- * SQLite adapter's ON CONFLICT guards, and the hub storages' change
964
- * ordering) — a drift class on the protocol's core convergence invariant.
965
- * Every implementation now derives from this module, and the golden-vector
966
- * conformance suite (exploration 0200) pins them equal.
1003
+ * The grinding-resistant LWW final tiebreak key (exploration 0305):
1004
+ * `blake3_hex( author ‖ US ‖ propertyKey ‖ US ‖ canonicalJSON(value) )`.
967
1005
  *
968
- * Ordering: lamport time, then wall time, then author DID compared by UTF-16
969
- * code units. NEVER `localeCompare` locale collation is non-deterministic
970
- * across ICU versions and would break CRDT convergence (see the
971
- * `0004-tie-author-case-codeunit` golden vector).
1006
+ * Portable by construction a kernel in any language reproduces it from
1007
+ * canonical JSON of the value (which every kernel already computes for the
1008
+ * change hash). A deletion (`value === undefined`) canonicalises as `null`.
972
1009
  */
973
- /** The timestamp triple every LWW comparison runs on. */
1010
+ declare function computeLwwTiebreakKey(author: string, propertyKey: string, value: unknown): string;
1011
+ /** The timestamp every LWW comparison runs on. */
974
1012
  interface LwwStamp {
975
1013
  lamport: number;
976
1014
  wallTime: number;
977
1015
  author: string;
1016
+ /**
1017
+ * Grinding-resistant tiebreak key ({@link computeLwwTiebreakKey}), present
1018
+ * only for changes at protocol version ≥ {@link LWW_TIEBREAK_KEY_VERSION}.
1019
+ * Absent (legacy) stamps fall back to the author-DID tiebreak.
1020
+ */
1021
+ tiebreakKey?: string;
978
1022
  }
979
1023
  /**
980
1024
  * Spec comparator (§L1.7): negative when `a` loses to `b`, positive when `a`
@@ -1000,13 +1044,22 @@ declare function compareChangeApplicationOrder(a: {
1000
1044
  * SQL `ON CONFLICT … DO UPDATE … WHERE` guard implementing {@link lwwWins}
1001
1045
  * inside SQLite (the `excluded.` pseudo-table is the incoming row). Column
1002
1046
  * text comparison (`>`) is byte order under SQLite's default BINARY
1003
- * collation, which matches the code-unit rule for our ASCII DID strings.
1047
+ * collation, which matches the code-unit rule for our ASCII DID/hex strings.
1048
+ *
1049
+ * When `tiebreakKeyColumn` is supplied, the final rung mirrors
1050
+ * {@link compareLwwStamps}: on a `lamport`+`wallTime` tie, if BOTH rows carry a
1051
+ * non-null tiebreak key the larger key wins outright (author irrelevant);
1052
+ * otherwise (either key null, or keys equal) the author DID decides. The stored
1053
+ * key is precomputed in application code ({@link computeLwwTiebreakKey}), so SQL
1054
+ * only ever compares the opaque hex — never recomputes it — keeping the JS and
1055
+ * SQL paths byte-identical without a user-defined function.
1004
1056
  */
1005
1057
  declare function lwwUpdateGuardSql(input: {
1006
1058
  table: string;
1007
1059
  lamportColumn: string;
1008
1060
  wallTimeColumn: string;
1009
1061
  authorColumn: string;
1062
+ tiebreakKeyColumn?: string;
1010
1063
  }): string;
1011
1064
 
1012
1065
  /**
@@ -1016,4 +1069,4 @@ declare function lwwUpdateGuardSql(input: {
1016
1069
  type DID = `did:key:${string}`;
1017
1070
  type DocumentPath = `xnet://${DID}/workspace/${string}/doc/${string}`;
1018
1071
 
1019
- 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 LwwStamp, type MembershipRoleResolver, 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 RetryPolicy, type Role, type RoleKey, type RoleRefExpr, type RoleResolver, STANDARD_ROLES, type SchemaAction, type SerializedAuthExpression, type SerializedAuthorization, type SerializedRoleResolver, type SignedUpdate, type SingleFlightOptions, type Snapshot, type SnapshotTriggers, SsrfError, type StreamingQueryOptions, type SubQuery, TaggedError, type TimeCondition, type UpdateVerifier, type VectorClock, assertPublicUrl, buildMerkleTree, capped, clamp, clamp01, compareChangeApplicationOrder, compareLwwStamps, compareVectorClocks, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, exponential, fixed, formatBytes, getMostPermissiveCapability, hashContent, incrementVectorClock, isLocationFresh, isTagged, isValidDID, isValidProgression, jittered, limitAttempts, lwwUpdateGuardSql, lwwWins, mergeStateVectors, mergeVectorClocks, parseContentId, parseDID, roleHasCapability, shouldCreateSnapshot, singleFlight, unionAggregate, validateExternalUrl, verifyContent, verifyUpdateChain };
1072
+ 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, LWW_TIEBREAK_KEY_VERSION, type LwwStamp, type MembershipRoleResolver, 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 RetryPolicy, type Role, type RoleKey, type RoleRefExpr, type RoleResolver, STANDARD_ROLES, type SchemaAction, type SerializedAuthExpression, type SerializedAuthorization, type SerializedRoleResolver, type SignedUpdate, type SingleFlightOptions, type Snapshot, type SnapshotTriggers, SsrfError, type StreamingQueryOptions, type SubQuery, TaggedError, type TimeCondition, type UpdateVerifier, type VectorClock, actionExpressionOrder, assertPublicUrl, buildMerkleTree, capped, clamp, clamp01, compareChangeApplicationOrder, compareLwwStamps, compareVectorClocks, computeLwwTiebreakKey, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, exponential, fixed, formatBytes, getMostPermissiveCapability, grantActionSatisfies, hashContent, incrementVectorClock, isLocationFresh, isTagged, isValidDID, isValidProgression, jittered, limitAttempts, lwwUpdateGuardSql, lwwWins, mergeStateVectors, mergeVectorClocks, parseContentId, parseDID, roleHasCapability, shouldCreateSnapshot, singleFlight, unionAggregate, validateExternalUrl, verifyContent, verifyUpdateChain };
package/dist/index.js CHANGED
@@ -267,8 +267,38 @@ function deduplicatedUnion(results) {
267
267
  return output;
268
268
  }
269
269
 
270
+ // src/auth-types.ts
271
+ var AUTH_ACTIONS = [
272
+ "read",
273
+ "create",
274
+ "update",
275
+ "write",
276
+ "delete",
277
+ "share",
278
+ "admin"
279
+ ];
280
+ function actionExpressionOrder(action) {
281
+ if (action === "create") return ["create", "write"];
282
+ if (action === "update" || action === "write") return ["update", "write"];
283
+ return [action];
284
+ }
285
+ function grantActionSatisfies(granted, checked) {
286
+ if (granted === checked) return true;
287
+ if (granted === "write") return checked === "create" || checked === "update";
288
+ if (granted === "update") return checked === "write";
289
+ return false;
290
+ }
291
+
270
292
  // src/permissions.ts
271
- var ALL_CAPABILITIES = ["read", "write", "delete", "share", "admin"];
293
+ var ALL_CAPABILITIES = [
294
+ "read",
295
+ "create",
296
+ "update",
297
+ "write",
298
+ "delete",
299
+ "share",
300
+ "admin"
301
+ ];
272
302
  var STANDARD_ROLES = {
273
303
  viewer: {
274
304
  id: "viewer",
@@ -284,7 +314,7 @@ var STANDARD_ROLES = {
284
314
  }
285
315
  };
286
316
  function roleHasCapability(role, capability) {
287
- return role.capabilities.includes(capability);
317
+ return role.capabilities.some((held) => grantActionSatisfies(held, capability));
288
318
  }
289
319
  function evaluateCondition(condition, context) {
290
320
  switch (condition.type) {
@@ -306,9 +336,6 @@ function getMostPermissiveCapability(capabilities) {
306
336
  return null;
307
337
  }
308
338
 
309
- // src/auth-types.ts
310
- var AUTH_ACTIONS = ["read", "write", "delete", "share", "admin"];
311
-
312
339
  // src/utils/math.ts
313
340
  function clamp(value, min, max) {
314
341
  if (value < min) return min;
@@ -470,9 +497,36 @@ function singleFlight(map, key, fn, options = {}) {
470
497
  }
471
498
 
472
499
  // src/lww.ts
500
+ import { blake3 as blake32 } from "@noble/hashes/blake3.js";
501
+ var LWW_TIEBREAK_KEY_VERSION = 4;
502
+ var US = "";
503
+ function canonicalize(value) {
504
+ if (value === null || typeof value !== "object") return value;
505
+ if (Array.isArray(value)) return value.map(canonicalize);
506
+ const out = {};
507
+ for (const key of Object.keys(value).sort()) {
508
+ out[key] = canonicalize(value[key]);
509
+ }
510
+ return out;
511
+ }
512
+ function toHex(bytes) {
513
+ let s = "";
514
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
515
+ return s;
516
+ }
517
+ function computeLwwTiebreakKey(author, propertyKey, value) {
518
+ const canonical = JSON.stringify(canonicalize(value === void 0 ? null : value));
519
+ const bytes = new TextEncoder().encode(`${author}${US}${propertyKey}${US}${canonical}`);
520
+ return toHex(blake32(bytes));
521
+ }
473
522
  function compareLwwStamps(a, b) {
474
523
  if (a.lamport !== b.lamport) return a.lamport - b.lamport;
475
524
  if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
525
+ const aK = a.tiebreakKey;
526
+ const bK = b.tiebreakKey;
527
+ if (aK !== void 0 && bK !== void 0 && aK !== bK) {
528
+ return aK < bK ? -1 : 1;
529
+ }
476
530
  return a.author < b.author ? -1 : a.author > b.author ? 1 : 0;
477
531
  }
478
532
  function lwwWins(incoming, existing) {
@@ -483,12 +537,19 @@ function compareChangeApplicationOrder(a, b) {
483
537
  return a.author < b.author ? -1 : a.author > b.author ? 1 : 0;
484
538
  }
485
539
  function lwwUpdateGuardSql(input) {
486
- const { table, lamportColumn, wallTimeColumn, authorColumn } = input;
540
+ const { table, lamportColumn, wallTimeColumn, authorColumn, tiebreakKeyColumn } = input;
541
+ const finalRung = tiebreakKeyColumn ? `(excluded.${tiebreakKeyColumn} IS NOT NULL
542
+ AND ${table}.${tiebreakKeyColumn} IS NOT NULL
543
+ AND excluded.${tiebreakKeyColumn} > ${table}.${tiebreakKeyColumn})
544
+ OR (NOT (excluded.${tiebreakKeyColumn} IS NOT NULL
545
+ AND ${table}.${tiebreakKeyColumn} IS NOT NULL
546
+ AND excluded.${tiebreakKeyColumn} <> ${table}.${tiebreakKeyColumn})
547
+ AND excluded.${authorColumn} > ${table}.${authorColumn})` : `excluded.${authorColumn} > ${table}.${authorColumn}`;
487
548
  return `excluded.${lamportColumn} > ${table}.${lamportColumn}
488
549
  OR (excluded.${lamportColumn} = ${table}.${lamportColumn}
489
550
  AND (excluded.${wallTimeColumn} > ${table}.${wallTimeColumn}
490
551
  OR (excluded.${wallTimeColumn} = ${table}.${wallTimeColumn}
491
- AND excluded.${authorColumn} > ${table}.${authorColumn})))`;
552
+ AND (${finalRung}))))`;
492
553
  }
493
554
  export {
494
555
  ALL_CAPABILITIES,
@@ -497,10 +558,12 @@ export {
497
558
  DEFAULT_SNAPSHOT_TRIGGERS,
498
559
  DEFAULT_STREAMING_OPTIONS,
499
560
  DHT_CONFIG,
561
+ LWW_TIEBREAK_KEY_VERSION,
500
562
  RESOLUTION_CACHE_CONFIG,
501
563
  STANDARD_ROLES,
502
564
  SsrfError,
503
565
  TaggedError,
566
+ actionExpressionOrder,
504
567
  assertPublicUrl,
505
568
  buildMerkleTree,
506
569
  capped,
@@ -509,6 +572,7 @@ export {
509
572
  compareChangeApplicationOrder,
510
573
  compareLwwStamps,
511
574
  compareVectorClocks,
575
+ computeLwwTiebreakKey,
512
576
  createChunk,
513
577
  createContentId,
514
578
  deduplicatedUnion,
@@ -519,6 +583,7 @@ export {
519
583
  fixed,
520
584
  formatBytes,
521
585
  getMostPermissiveCapability,
586
+ grantActionSatisfies,
522
587
  hashContent,
523
588
  incrementVectorClock,
524
589
  isLocationFresh,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/core",
3
- "version": "0.12.0",
3
+ "version": "1.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",