@resourcexjs/core 2.14.1 → 2.15.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
@@ -72,18 +72,21 @@ interface RXI {
72
72
  readonly path?: string;
73
73
  /** Resource name */
74
74
  readonly name: string;
75
- /** Tag (version or label). Defaults to "latest" if not specified. */
75
+ /** Tag (mutable pointer). Defaults to "latest" if not specified. */
76
76
  readonly tag: string;
77
+ /** Content digest (immutable hash, e.g., "sha256:abc123"). */
78
+ readonly digest?: string;
77
79
  }
78
80
  /**
79
81
  * Format RXI to locator string.
80
82
  *
81
- * Docker-style format: [registry/][path/]name[:tag]
83
+ * Docker-style format: [registry/][path/]name[:tag][@digest]
82
84
  *
83
85
  * Examples:
84
86
  * - { name: "hello", tag: "latest" } → "hello" (omit :latest)
85
87
  * - { name: "hello", tag: "1.0.0" } → "hello:1.0.0"
86
- * - { registry: "localhost:3098", name: "hello", tag: "1.0.0" } → "localhost:3098/hello:1.0.0"
88
+ * - { name: "hello", tag: "latest", digest: "sha256:abc" } → "hello@sha256:abc"
89
+ * - { name: "hello", tag: "beta", digest: "sha256:abc" } → "hello:beta@sha256:abc"
87
90
  *
88
91
  * @param rxi - Resource identifier
89
92
  * @returns Locator string
@@ -113,9 +116,11 @@ interface RXMDefinition {
113
116
  }
114
117
  /**
115
118
  * RXM Archive — packaging metadata.
116
- * Placeholder for future fields (digest, size, md5, etc.)
117
119
  */
118
- type RXMArchive = {};
120
+ interface RXMArchive {
121
+ /** Deterministic content digest computed from file-level digests. Format: sha256:<hex> */
122
+ readonly digest?: string;
123
+ }
119
124
  /**
120
125
  * File entry with metadata.
121
126
  */
@@ -242,7 +247,7 @@ interface TypeDetectionResult {
242
247
  readonly type: string;
243
248
  /** Detected resource name */
244
249
  readonly name: string;
245
- /** Tag/version (defaults to "latest" if not provided) */
250
+ /** Tag (defaults to "latest" if not provided) */
246
251
  readonly tag?: string;
247
252
  /** Description extracted from content */
248
253
  readonly description?: string;
@@ -415,21 +420,6 @@ interface SourceLoader {
415
420
  * @throws ResourceXError if loading fails
416
421
  */
417
422
  load(source: string): Promise<RXS>;
418
- /**
419
- * Check if cached content for this source is still fresh.
420
- *
421
- * Each loader implements its own strategy:
422
- * - FolderSourceLoader: compare file mtime against cachedAt
423
- * - GitHubSourceLoader: not implemented (always stale)
424
- *
425
- * Loaders that don't implement this are treated as always stale,
426
- * causing a full reload on every ingest.
427
- *
428
- * @param source - Source path or identifier
429
- * @param cachedAt - When the resource was last cached
430
- * @returns true if cache is still fresh, false if stale
431
- */
432
- isFresh?(source: string, cachedAt: Date): Promise<boolean>;
433
423
  }
434
424
  /**
435
425
  * Default ResourceLoader implementation for loading resources from folders.
@@ -446,8 +436,8 @@ interface SourceLoader {
446
436
  * {
447
437
  * "name": "resource-name", // required
448
438
  * "type": "text", // required
449
- * "version": "1.0.0", // required
450
- * "domain": "localhost", // optional, defaults to "localhost"
439
+ * "tag": "1.0.0", // optional, defaults to "latest"
440
+ * "registry": "localhost", // optional
451
441
  * "path": "optional/path" // optional
452
442
  * }
453
443
  * ```
@@ -473,15 +463,6 @@ declare class FolderSourceLoader implements SourceLoader {
473
463
  canLoad(source: string): Promise<boolean>;
474
464
  load(source: string): Promise<RXS>;
475
465
  /**
476
- * Check if cached content is still fresh by comparing file mtimes.
477
- * Returns true only if no file in the directory has been modified since cachedAt.
478
- */
479
- isFresh(source: string, cachedAt: Date): Promise<boolean>;
480
- /**
481
- * Get the most recent mtime across all files in a folder (recursive).
482
- */
483
- private getMaxMtime;
484
- /**
485
466
  * Recursively read all files in a folder.
486
467
  */
487
468
  private readFolderFiles;
@@ -584,17 +565,6 @@ declare class SourceLoaderChain {
584
565
  * @throws ResourceXError if no loader matches
585
566
  */
586
567
  load(source: string): Promise<RXS>;
587
- /**
588
- * Check if cached content for a source is still fresh.
589
- *
590
- * Delegates to the matching loader's isFresh method.
591
- * Returns false if the loader doesn't implement isFresh (always reload).
592
- *
593
- * @param source - Source path or identifier
594
- * @param cachedAt - When the resource was last cached
595
- * @returns true if cache is still fresh
596
- */
597
- isFresh(source: string, cachedAt: Date): Promise<boolean>;
598
568
  }
599
569
  /**
600
570
  * Configuration for resolveSource.
@@ -684,6 +654,7 @@ interface StoredRXM {
684
654
  readonly license?: string;
685
655
  readonly keywords?: string[];
686
656
  readonly repository?: string;
657
+ readonly digest?: string;
687
658
  readonly files: Record<string, string>;
688
659
  readonly createdAt?: Date;
689
660
  readonly updatedAt?: Date;
@@ -778,19 +749,6 @@ interface ProviderStores {
778
749
  rxmStore: RXMStore;
779
750
  }
780
751
  /**
781
- * Resource loader interface for loading from directories/archives.
782
- */
783
- interface ResourceLoader2 {
784
- /**
785
- * Check if this loader can handle the given source.
786
- */
787
- canLoad(source: string): boolean | Promise<boolean>;
788
- /**
789
- * Load resource from source.
790
- */
791
- load(source: string): Promise<unknown>;
792
- }
793
- /**
794
752
  * Platform-specific defaults resolved from environment variables and config files.
795
753
  */
796
754
  interface ProviderDefaults {
@@ -823,11 +781,6 @@ interface ResourceXProvider {
823
781
  */
824
782
  createStores(config: ProviderConfig): ProviderStores;
825
783
  /**
826
- * Create resource loader (optional).
827
- * Not all platforms support loading from filesystem.
828
- */
829
- createLoader?(config: ProviderConfig): ResourceLoader2;
830
- /**
831
784
  * Create source loader for auto-detection pipeline (optional).
832
785
  */
833
786
  createSourceLoader?(config: ProviderConfig): SourceLoader;
@@ -915,8 +868,9 @@ interface Registry {
915
868
  get(rxi: RXI): Promise<RXR>;
916
869
  /**
917
870
  * Store resource.
871
+ * @returns The stored manifest (with computed digest).
918
872
  */
919
- put(rxr: RXR): Promise<void>;
873
+ put(rxr: RXR): Promise<RXM>;
920
874
  /**
921
875
  * Check if resource exists.
922
876
  */
@@ -936,7 +890,7 @@ declare class CASRegistry implements Registry {
936
890
  constructor(rxaStore: RXAStore, rxmStore: RXMStore);
937
891
  private resolveTag;
938
892
  get(rxi: RXI): Promise<RXR>;
939
- put(rxr: RXR): Promise<void>;
893
+ put(rxr: RXR): Promise<RXM>;
940
894
  has(rxi: RXI): Promise<boolean>;
941
895
  remove(rxi: RXI): Promise<void>;
942
896
  list(options?: SearchOptions): Promise<RXI[]>;
@@ -994,7 +948,7 @@ declare class LinkedRegistry implements Registry {
994
948
  * Put is not typically used for LinkedRegistry.
995
949
  * Use link() instead to create symlinks.
996
950
  */
997
- put(_rxr: RXR): Promise<void>;
951
+ put(_rxr: RXR): Promise<RXM>;
998
952
  has(rxi: RXI): Promise<boolean>;
999
953
  remove(rxi: RXI): Promise<void>;
1000
954
  list(options?: SearchOptions): Promise<RXI[]>;
@@ -1030,7 +984,7 @@ declare abstract class RegistryMiddleware implements Registry {
1030
984
  protected readonly inner: Registry;
1031
985
  constructor(inner: Registry);
1032
986
  get(rxi: RXI): Promise<RXR>;
1033
- put(rxr: RXR): Promise<void>;
987
+ put(rxr: RXR): Promise<RXM>;
1034
988
  has(rxi: RXI): Promise<boolean>;
1035
989
  remove(rxi: RXI): Promise<void>;
1036
990
  list(options?: SearchOptions): Promise<RXI[]>;
@@ -1129,7 +1083,7 @@ interface ResolveContext {
1129
1083
  path?: string
1130
1084
  name: string
1131
1085
  type: string
1132
- version: string
1086
+ tag: string
1133
1087
  };
1134
1088
  /**
1135
1089
  * Extracted files from archive.
package/dist/index.js CHANGED
@@ -14597,7 +14597,6 @@ var RXDSchema = exports_external.object({
14597
14597
  name: exports_external.string().min(1).max(128),
14598
14598
  type: exports_external.string().min(1).max(64),
14599
14599
  tag: exports_external.string().max(64).optional(),
14600
- version: exports_external.string().max(64).optional(),
14601
14600
  registry: exports_external.string().max(256).optional(),
14602
14601
  path: exports_external.string().max(256).optional(),
14603
14602
  description: exports_external.string().max(1024).optional(),
@@ -14619,7 +14618,7 @@ function define(input) {
14619
14618
  const rxd = Object.assign(Object.create(null), {
14620
14619
  name: validated.name,
14621
14620
  type: validated.type,
14622
- tag: validated.tag ?? validated.version ?? undefined,
14621
+ tag: validated.tag ?? undefined,
14623
14622
  registry: validated.registry,
14624
14623
  path: validated.path,
14625
14624
  description: validated.description,
@@ -14659,6 +14658,9 @@ function format(rxi) {
14659
14658
  if (rxi.tag && rxi.tag !== "latest") {
14660
14659
  result += `:${rxi.tag}`;
14661
14660
  }
14661
+ if (rxi.digest) {
14662
+ result += `@${rxi.digest}`;
14663
+ }
14662
14664
  return result;
14663
14665
  }
14664
14666
  // src/model/locate.ts
@@ -14737,15 +14739,25 @@ function parse5(locator) {
14737
14739
  throw new LocatorError("Locator must be a non-empty string", locator);
14738
14740
  }
14739
14741
  validateLocatorSecurity(locator);
14740
- if (locator.includes("@")) {
14741
- throw new LocatorError("Invalid locator format. Use name:tag instead of name@version", locator);
14742
+ let digest;
14743
+ let locatorWithoutDigest = locator;
14744
+ const atIndex = locator.indexOf("@");
14745
+ if (atIndex !== -1) {
14746
+ if (atIndex === 0) {
14747
+ throw new LocatorError("Invalid locator format. Name is required before @", locator);
14748
+ }
14749
+ digest = locator.substring(atIndex + 1);
14750
+ locatorWithoutDigest = locator.substring(0, atIndex);
14751
+ if (!digest || digest.includes("@")) {
14752
+ throw new LocatorError("Invalid digest format after @", locator);
14753
+ }
14742
14754
  }
14743
- const lastSlashIndex = locator.lastIndexOf("/");
14755
+ const lastSlashIndex = locatorWithoutDigest.lastIndexOf("/");
14744
14756
  let beforeSlash = "";
14745
- let afterSlash = locator;
14757
+ let afterSlash = locatorWithoutDigest;
14746
14758
  if (lastSlashIndex !== -1) {
14747
- beforeSlash = locator.substring(0, lastSlashIndex);
14748
- afterSlash = locator.substring(lastSlashIndex + 1);
14759
+ beforeSlash = locatorWithoutDigest.substring(0, lastSlashIndex);
14760
+ afterSlash = locatorWithoutDigest.substring(lastSlashIndex + 1);
14749
14761
  }
14750
14762
  const colonIndex = afterSlash.lastIndexOf(":");
14751
14763
  let name;
@@ -14768,7 +14780,8 @@ function parse5(locator) {
14768
14780
  registry: undefined,
14769
14781
  path: undefined,
14770
14782
  name,
14771
- tag
14783
+ tag,
14784
+ digest
14772
14785
  };
14773
14786
  }
14774
14787
  const parts = beforeSlash.split("/");
@@ -14779,14 +14792,16 @@ function parse5(locator) {
14779
14792
  registry: registry2,
14780
14793
  path,
14781
14794
  name,
14782
- tag
14795
+ tag,
14796
+ digest
14783
14797
  };
14784
14798
  }
14785
14799
  return {
14786
14800
  registry: undefined,
14787
14801
  path: beforeSlash,
14788
14802
  name,
14789
- tag
14803
+ tag,
14804
+ digest
14790
14805
  };
14791
14806
  }
14792
14807
  // src/model/resource.ts
@@ -14864,7 +14879,7 @@ class ResourceJsonDetector {
14864
14879
  return {
14865
14880
  type: json2.type,
14866
14881
  name: json2.name,
14867
- tag: json2.tag ?? json2.version ?? undefined,
14882
+ tag: json2.tag,
14868
14883
  description: json2.description,
14869
14884
  registry: json2.registry,
14870
14885
  path: json2.path,
@@ -15008,31 +15023,6 @@ class FolderSourceLoader {
15008
15023
  const files = await this.readFolderFiles(source);
15009
15024
  return { source, files };
15010
15025
  }
15011
- async isFresh(source, cachedAt) {
15012
- try {
15013
- const maxMtime = await this.getMaxMtime(source);
15014
- return maxMtime <= cachedAt;
15015
- } catch {
15016
- return false;
15017
- }
15018
- }
15019
- async getMaxMtime(folderPath) {
15020
- let max = new Date(0);
15021
- const entries = await readdir2(folderPath, { withFileTypes: true });
15022
- for (const entry of entries) {
15023
- const fullPath = join2(folderPath, entry.name);
15024
- if (entry.isFile()) {
15025
- const stats = await stat2(fullPath);
15026
- if (stats.mtime > max)
15027
- max = stats.mtime;
15028
- } else if (entry.isDirectory()) {
15029
- const subMax = await this.getMaxMtime(fullPath);
15030
- if (subMax > max)
15031
- max = subMax;
15032
- }
15033
- }
15034
- return max;
15035
- }
15036
15026
  async readFolderFiles(folderPath, basePath = folderPath) {
15037
15027
  const files = {};
15038
15028
  const entries = await readdir2(folderPath, { withFileTypes: true });
@@ -15154,17 +15144,6 @@ class SourceLoaderChain {
15154
15144
  }
15155
15145
  throw new ResourceXError(`Cannot load source: ${source}`);
15156
15146
  }
15157
- async isFresh(source, cachedAt) {
15158
- for (const loader of this.loaders) {
15159
- if (await loader.canLoad(source)) {
15160
- if (loader.isFresh) {
15161
- return loader.isFresh(source, cachedAt);
15162
- }
15163
- return false;
15164
- }
15165
- }
15166
- return false;
15167
- }
15168
15147
  }
15169
15148
 
15170
15149
  // src/loader/resolveSource.ts
@@ -15278,6 +15257,22 @@ function withRegistryValidation(registry2, trustedRegistry) {
15278
15257
  }
15279
15258
  var DomainValidation = RegistryValidation;
15280
15259
  var withDomainValidation = withRegistryValidation;
15260
+ // src/registry/store/digest.ts
15261
+ import { createHash } from "node:crypto";
15262
+ function computeDigest(data) {
15263
+ const hash2 = createHash("sha256").update(data).digest("hex");
15264
+ return `sha256:${hash2}`;
15265
+ }
15266
+ function computeArchiveDigest(files) {
15267
+ const entries = Object.keys(files).sort().map((name) => `${name}:${files[name]}`).join(`
15268
+ `);
15269
+ const hash2 = createHash("sha256").update(entries).digest("hex");
15270
+ return `sha256:${hash2}`;
15271
+ }
15272
+ function isValidDigest(digest) {
15273
+ return /^sha256:[a-f0-9]{64}$/.test(digest);
15274
+ }
15275
+
15281
15276
  // src/registry/registries/CASRegistry.ts
15282
15277
  class CASRegistry {
15283
15278
  rxaStore;
@@ -15320,7 +15315,9 @@ class CASRegistry {
15320
15315
  keywords: storedRxm.keywords,
15321
15316
  repository: storedRxm.repository
15322
15317
  },
15323
- archive: {},
15318
+ archive: {
15319
+ digest: storedRxm.digest ?? computeArchiveDigest(storedRxm.files)
15320
+ },
15324
15321
  source: {}
15325
15322
  };
15326
15323
  const rxa = await archive(files);
@@ -15330,9 +15327,10 @@ class CASRegistry {
15330
15327
  const files = await extract(rxr.archive);
15331
15328
  const fileDigests = {};
15332
15329
  for (const [filename, content] of Object.entries(files)) {
15333
- const digest = await this.rxaStore.put(content);
15334
- fileDigests[filename] = digest;
15330
+ const digest2 = await this.rxaStore.put(content);
15331
+ fileDigests[filename] = digest2;
15335
15332
  }
15333
+ const digest = computeArchiveDigest(fileDigests);
15336
15334
  const storedRxm = {
15337
15335
  registry: rxr.manifest.definition.registry,
15338
15336
  path: rxr.manifest.definition.path,
@@ -15344,12 +15342,18 @@ class CASRegistry {
15344
15342
  license: rxr.manifest.definition.license,
15345
15343
  keywords: rxr.manifest.definition.keywords,
15346
15344
  repository: rxr.manifest.definition.repository,
15345
+ digest,
15347
15346
  files: fileDigests,
15348
15347
  createdAt: new Date,
15349
15348
  updatedAt: new Date
15350
15349
  };
15351
15350
  await this.rxmStore.put(storedRxm);
15352
15351
  await this.rxmStore.setLatest(rxr.manifest.definition.name, rxr.manifest.definition.tag, rxr.manifest.definition.registry);
15352
+ return {
15353
+ definition: rxr.manifest.definition,
15354
+ archive: { digest },
15355
+ source: rxr.manifest.source
15356
+ };
15353
15357
  }
15354
15358
  async has(rxi) {
15355
15359
  const tag = await this.resolveTag(rxi.name, rxi.tag ?? "latest", rxi.registry);
@@ -15548,15 +15552,6 @@ class LinkedRegistry {
15548
15552
  }
15549
15553
  }
15550
15554
  }
15551
- // src/registry/store/digest.ts
15552
- import { createHash } from "node:crypto";
15553
- function computeDigest(data) {
15554
- const hash2 = createHash("sha256").update(data).digest("hex");
15555
- return `sha256:${hash2}`;
15556
- }
15557
- function isValidDigest(digest) {
15558
- return /^sha256:[a-f0-9]{64}$/.test(digest);
15559
- }
15560
15555
  // src/registry/store/MemoryRXAStore.ts
15561
15556
  class MemoryRXAStore {
15562
15557
  blobs = new Map;
@@ -15904,4 +15899,4 @@ export {
15904
15899
  CASRegistry
15905
15900
  };
15906
15901
 
15907
- //# debugId=B122DF8CA052DA8364756E2164756E21
15902
+ //# debugId=2D991EC1BFB2AAA764756E2164756E21