@xnetjs/core 0.0.2 → 0.1.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
@@ -699,7 +699,7 @@ interface AuthenticatedExpr {
699
699
  /**
700
700
  * How to determine if a user holds a role.
701
701
  */
702
- type RoleResolver = CreatorRoleResolver | PropertyRoleResolver | RelationRoleResolver;
702
+ type RoleResolver = CreatorRoleResolver | PropertyRoleResolver | RelationRoleResolver | MembershipRoleResolver;
703
703
  /**
704
704
  * Serialized form of RoleResolver for JSON storage.
705
705
  */
@@ -712,6 +712,15 @@ type SerializedRoleResolver = {
712
712
  _tag: 'relation';
713
713
  relationName: string;
714
714
  targetRole: string;
715
+ } | {
716
+ _tag: 'membership';
717
+ edgeSchema: string;
718
+ containerProp: string;
719
+ memberProp: string;
720
+ roleProp: string;
721
+ minRole: string;
722
+ roleOrder: string[];
723
+ parentProp?: string;
715
724
  };
716
725
  /**
717
726
  * Role held by the node's creator.
@@ -736,6 +745,34 @@ interface RelationRoleResolver {
736
745
  readonly relationName: string;
737
746
  readonly targetRole: string;
738
747
  }
748
+ /**
749
+ * Role determined by membership edges that point at THIS node (a reverse-edge
750
+ * lookup the forward `relation`/`property` resolvers can't express).
751
+ *
752
+ * Given a container node (e.g. a Space), the subject holds this role when an
753
+ * edge node of `edgeSchema` exists whose `containerProp` references this node
754
+ * (or, when `parentProp` is set, any of its ancestors), whose `memberProp`
755
+ * holds the subject DID, and whose `roleProp` rank is `>= minRole` per the
756
+ * `roleOrder` ladder (least → most privileged). The ancestor walk is how
757
+ * membership cascades down a nested container tree without fanning grants out.
758
+ */
759
+ interface MembershipRoleResolver {
760
+ readonly _tag: 'membership';
761
+ /** Schema IRI of the membership edge node (e.g. SpaceMembership). */
762
+ readonly edgeSchema: string;
763
+ /** Edge property that references the container node. */
764
+ readonly containerProp: string;
765
+ /** Edge property holding the member DID. */
766
+ readonly memberProp: string;
767
+ /** Edge property holding the member's role id. */
768
+ readonly roleProp: string;
769
+ /** Minimum role rung this resolver represents. */
770
+ readonly minRole: string;
771
+ /** Role ids ordered least → most privileged (for rank comparison). */
772
+ readonly roleOrder: readonly string[];
773
+ /** Container relation to walk for ancestor inheritance (e.g. `parent`). */
774
+ readonly parentProp?: string;
775
+ }
739
776
  /**
740
777
  * Input for an authorization check.
741
778
  */
@@ -770,6 +807,63 @@ interface PolicyEvaluator {
770
807
  invalidateSubject(did: DID$1): void;
771
808
  }
772
809
 
810
+ /**
811
+ * Small, dependency-free numeric helpers.
812
+ *
813
+ * These were independently re-implemented across many packages; this is the
814
+ * single canonical home for the behaviour-identical variants. Specialized
815
+ * clampers (`clampRatio`, `clampLimit`, `clampInteger`, …) intentionally stay
816
+ * local to their callers — they encode caller-specific fallbacks and bounds.
817
+ */
818
+ /** Clamp `value` into the inclusive `[min, max]` range. */
819
+ declare function clamp(value: number, min: number, max: number): number;
820
+ /** Clamp `value` into the inclusive `[0, 1]` range. */
821
+ declare function clamp01(value: number): number;
822
+
823
+ /**
824
+ * Small, dependency-free formatting helpers shared across packages.
825
+ */
826
+ /**
827
+ * Format a byte count into a human-readable string using binary (1024) units.
828
+ *
829
+ * Canonical replacement for the several `formatBytes`/`formatFileSize` copies
830
+ * that diverged across packages — notably ones that silently capped at MB and
831
+ * mis-reported gigabyte-scale sizes. Scales all the way to PB and keeps one
832
+ * decimal place above the byte unit (e.g. `1536` → `"1.5 KB"`, `0` → `"0 B"`).
833
+ */
834
+ declare function formatBytes(bytes: number): string;
835
+
836
+ /**
837
+ * SSRF guard for server-side / outbound requests.
838
+ *
839
+ * A *literal-host* guard (scheme + hostname/IP inspection), not a
840
+ * post-DNS-resolution guard: a hostname that resolves to a private IP at
841
+ * request time is not caught here — that deeper check belongs in the fetch
842
+ * implementation. This closes the common, cheap holes by construction:
843
+ * non-http(s) schemes, `localhost`, `.local`/`.internal` suffixes, the cloud
844
+ * metadata host, and private/loopback/link-local IP literals (v4 and v6).
845
+ *
846
+ * This is the canonical implementation; `@xnetjs/hub` and `@xnetjs/plugins`
847
+ * both delegate here so the IP-range logic lives in exactly one place.
848
+ */
849
+ declare class SsrfError extends Error {
850
+ readonly url: string;
851
+ constructor(message: string, url: string);
852
+ }
853
+ /**
854
+ * Throw {@link SsrfError} unless `rawUrl` is a plausibly-public HTTP(S)
855
+ * endpoint.
856
+ */
857
+ declare function assertPublicUrl(rawUrl: string): void;
858
+ /**
859
+ * Non-throwing variant of {@link assertPublicUrl}: returns `{ valid: true }`
860
+ * for a public HTTP(S) URL, or `{ valid: false, error }` otherwise.
861
+ */
862
+ declare function validateExternalUrl(rawUrl: string): {
863
+ valid: boolean;
864
+ error?: string;
865
+ };
866
+
773
867
  /**
774
868
  * @xnetjs/core - Core types, schemas, and content addressing
775
869
  */
@@ -777,4 +871,4 @@ interface PolicyEvaluator {
777
871
  type DID = `did:key:${string}`;
778
872
  type DocumentPath = `xnet://${DID}/workspace/${string}/doc/${string}`;
779
873
 
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 };
874
+ 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 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 Role, type RoleKey, type RoleRefExpr, type RoleResolver, STANDARD_ROLES, type SchemaAction, type SerializedAuthExpression, type SerializedAuthorization, type SerializedRoleResolver, type SignedUpdate, type Snapshot, type SnapshotTriggers, SsrfError, type StreamingQueryOptions, type SubQuery, type TimeCondition, type UpdateVerifier, type VectorClock, assertPublicUrl, buildMerkleTree, clamp, clamp01, compareVectorClocks, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, formatBytes, getMostPermissiveCapability, hashContent, incrementVectorClock, isLocationFresh, isValidDID, isValidProgression, mergeStateVectors, mergeVectorClocks, parseContentId, parseDID, roleHasCapability, shouldCreateSnapshot, unionAggregate, validateExternalUrl, verifyContent, verifyUpdateChain };
package/dist/index.js CHANGED
@@ -308,6 +308,105 @@ function getMostPermissiveCapability(capabilities) {
308
308
 
309
309
  // src/auth-types.ts
310
310
  var AUTH_ACTIONS = ["read", "write", "delete", "share", "admin"];
311
+
312
+ // src/utils/math.ts
313
+ function clamp(value, min, max) {
314
+ if (value < min) return min;
315
+ if (value > max) return max;
316
+ return value;
317
+ }
318
+ function clamp01(value) {
319
+ return clamp(value, 0, 1);
320
+ }
321
+
322
+ // src/utils/format.ts
323
+ var BYTE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
324
+ function formatBytes(bytes) {
325
+ if (!Number.isFinite(bytes)) return "\u2014";
326
+ const negative = bytes < 0;
327
+ let value = Math.abs(bytes);
328
+ if (value < 1024) return `${negative ? "-" : ""}${value} B`;
329
+ let unitIndex = 0;
330
+ while (value >= 1024 && unitIndex < BYTE_UNITS.length - 1) {
331
+ value /= 1024;
332
+ unitIndex += 1;
333
+ }
334
+ return `${negative ? "-" : ""}${value.toFixed(1)} ${BYTE_UNITS[unitIndex]}`;
335
+ }
336
+
337
+ // src/utils/ssrf.ts
338
+ var SsrfError = class extends Error {
339
+ constructor(message, url) {
340
+ super(message);
341
+ this.url = url;
342
+ this.name = "SsrfError";
343
+ }
344
+ };
345
+ function parseIpv4(host) {
346
+ const parts = host.split(".");
347
+ if (parts.length !== 4) return null;
348
+ let value = 0;
349
+ for (const part of parts) {
350
+ if (!/^\d{1,3}$/.test(part)) return null;
351
+ const octet = Number(part);
352
+ if (octet > 255) return null;
353
+ value = value * 256 + octet;
354
+ }
355
+ return value >>> 0;
356
+ }
357
+ function isPrivateIpv4(host) {
358
+ const ip = parseIpv4(host);
359
+ if (ip === null) return false;
360
+ const inRange = (base, bits) => {
361
+ const baseIp = parseIpv4(base);
362
+ const mask = bits === 0 ? 0 : 4294967295 << 32 - bits >>> 0;
363
+ return (ip & mask) === (baseIp & mask);
364
+ };
365
+ return inRange("0.0.0.0", 8) || // "this" network / 0.0.0.0
366
+ inRange("10.0.0.0", 8) || // private
367
+ inRange("127.0.0.0", 8) || // loopback
368
+ inRange("169.254.0.0", 16) || // link-local + cloud metadata (169.254.169.254)
369
+ inRange("172.16.0.0", 12) || // private
370
+ inRange("192.168.0.0", 16) || // private
371
+ inRange("100.64.0.0", 10);
372
+ }
373
+ function isBlockedIpv6(host) {
374
+ const h = host.replace(/^\[|\]$/g, "").toLowerCase();
375
+ if (h === "::1" || h === "::") return true;
376
+ if (h.startsWith("fc") || h.startsWith("fd")) return true;
377
+ if (h.startsWith("::ffff:") || h.startsWith("64:ff9b::")) return true;
378
+ const firstHextet = parseInt(h.split(":")[0] || "0", 16);
379
+ if (Number.isFinite(firstHextet) && (firstHextet & 65472) === 65152) return true;
380
+ return false;
381
+ }
382
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "metadata.google.internal"]);
383
+ function assertPublicUrl(rawUrl) {
384
+ let url;
385
+ try {
386
+ url = new URL(rawUrl);
387
+ } catch {
388
+ throw new SsrfError(`URL is not valid: ${rawUrl}`, rawUrl);
389
+ }
390
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
391
+ throw new SsrfError(`URL must be http(s): ${rawUrl}`, rawUrl);
392
+ }
393
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
394
+ if (host.length === 0) {
395
+ throw new SsrfError(`URL has no hostname: ${rawUrl}`, rawUrl);
396
+ }
397
+ const blocked = BLOCKED_HOSTNAMES.has(host) || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal") || (host.startsWith("[") ? isBlockedIpv6(host) : isPrivateIpv4(host));
398
+ if (blocked) {
399
+ throw new SsrfError(`URL targets a non-public host: ${host}`, rawUrl);
400
+ }
401
+ }
402
+ function validateExternalUrl(rawUrl) {
403
+ try {
404
+ assertPublicUrl(rawUrl);
405
+ return { valid: true };
406
+ } catch (error) {
407
+ return { valid: false, error: error instanceof Error ? error.message : "Invalid URL" };
408
+ }
409
+ }
311
410
  export {
312
411
  ALL_CAPABILITIES,
313
412
  AUTH_ACTIONS,
@@ -317,7 +416,11 @@ export {
317
416
  DHT_CONFIG,
318
417
  RESOLUTION_CACHE_CONFIG,
319
418
  STANDARD_ROLES,
419
+ SsrfError,
420
+ assertPublicUrl,
320
421
  buildMerkleTree,
422
+ clamp,
423
+ clamp01,
321
424
  compareVectorClocks,
322
425
  createChunk,
323
426
  createContentId,
@@ -325,6 +428,7 @@ export {
325
428
  detectFork,
326
429
  estimateQueryCost,
327
430
  evaluateCondition,
431
+ formatBytes,
328
432
  getMostPermissiveCapability,
329
433
  hashContent,
330
434
  incrementVectorClock,
@@ -338,6 +442,7 @@ export {
338
442
  roleHasCapability,
339
443
  shouldCreateSnapshot,
340
444
  unionAggregate,
445
+ validateExternalUrl,
341
446
  verifyContent,
342
447
  verifyUpdateChain
343
448
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/core",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",