capdag 0.152.345 → 0.153.347

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.
Files changed (3) hide show
  1. package/capdag.js +130 -20
  2. package/capdag.test.js +46 -20
  3. package/package.json +1 -1
package/capdag.js CHANGED
@@ -3613,6 +3613,54 @@ class CartridgeCapGroup {
3613
3613
  }
3614
3614
  }
3615
3615
 
3616
+ // =============================================================================
3617
+ // Cartridge registry slug
3618
+ // =============================================================================
3619
+ //
3620
+ // Deterministic mapping from a registry URL to a top-level folder
3621
+ // name under the cartridges install root. Mirrors
3622
+ // capdag::cartridge_slug byte-for-byte: SHA-256 of the URL bytes,
3623
+ // lowercase hex, first 16 chars. The literal string "dev" is
3624
+ // reserved for dev cartridges that have no registry.
3625
+ //
3626
+ // JS uses Web Crypto's SubtleCrypto for SHA-256. The function is
3627
+ // async because `crypto.subtle.digest` returns a Promise; consumers
3628
+ // in synchronous contexts must await.
3629
+
3630
+ const DEV_SLUG = "dev";
3631
+ const SLUG_HEX_LEN = 16;
3632
+
3633
+ /**
3634
+ * Compute the on-disk slug for a registry URL.
3635
+ *
3636
+ * @param {string|null|undefined} registryUrl - The registry URL, or
3637
+ * null/undefined for dev installs.
3638
+ * @returns {Promise<string>} The slug — `DEV_SLUG` for null,
3639
+ * otherwise the first 16 lowercase-hex characters of
3640
+ * sha256(registryUrl).
3641
+ */
3642
+ async function slugForRegistryUrl(registryUrl) {
3643
+ if (registryUrl === null || registryUrl === undefined) {
3644
+ return DEV_SLUG;
3645
+ }
3646
+ const bytes = new TextEncoder().encode(registryUrl);
3647
+ // Web Crypto exposes SHA-256 via crypto.subtle. Node 16+ exposes
3648
+ // it through globalThis.crypto.subtle.
3649
+ const digestBuffer = await crypto.subtle.digest("SHA-256", bytes);
3650
+ const digestBytes = new Uint8Array(digestBuffer);
3651
+ let hex = "";
3652
+ for (const b of digestBytes) {
3653
+ hex += b.toString(16).padStart(2, "0");
3654
+ }
3655
+ return hex.slice(0, SLUG_HEX_LEN);
3656
+ }
3657
+
3658
+ function isRegistrySlug(s) {
3659
+ return typeof s === "string"
3660
+ && s.length === SLUG_HEX_LEN
3661
+ && /^[0-9a-f]+$/.test(s);
3662
+ }
3663
+
3616
3664
  /**
3617
3665
  * Cartridge information from registry
3618
3666
  */
@@ -3640,6 +3688,16 @@ class CartridgeInfo {
3640
3688
  throw new Error(`CartridgeInfo ${data.id || '?'}: invalid or missing channel '${data.channel}'`);
3641
3689
  }
3642
3690
  this.channel = data.channel;
3691
+ // Registry URL: verbatim string the registry was fetched from.
3692
+ // Required and non-empty — every CartridgeInfo carries the URL of
3693
+ // the registry that served it so downstream consumers can build
3694
+ // the (registryUrl, channel, id) identity tuple without
3695
+ // re-deriving it. The registry transformer stamps this onto every
3696
+ // entry at flatten time.
3697
+ if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
3698
+ throw new Error(`CartridgeInfo ${data.id || '?'}: registryUrl is required and must be a non-empty string`);
3699
+ }
3700
+ this.registryUrl = data.registryUrl;
3643
3701
  }
3644
3702
 
3645
3703
  /** All caps flattened across all cap_groups, deduplicated by URN */
@@ -3688,9 +3746,13 @@ class CartridgeInfo {
3688
3746
  }
3689
3747
 
3690
3748
  /**
3691
- * Cartridge suggestion for a missing cap. `channel` reports which
3692
- * channel the suggesting cartridge lives in so consumers can render
3693
- * the release/nightly distinction.
3749
+ * Cartridge suggestion for a missing cap.
3750
+ *
3751
+ * `(registryUrl, channel, cartridgeId)` is the suggesting
3752
+ * cartridge's full identity — installs of the same id from
3753
+ * different registries × channels are independent records and the
3754
+ * client keeps both visible. `registryUrl` is required and
3755
+ * non-empty; suggestions never come from dev installs.
3694
3756
  */
3695
3757
  class CartridgeSuggestion {
3696
3758
  constructor(data) {
@@ -3706,25 +3768,29 @@ class CartridgeSuggestion {
3706
3768
  throw new Error(`CartridgeSuggestion: invalid or missing channel '${data.channel}'`);
3707
3769
  }
3708
3770
  this.channel = data.channel;
3771
+ if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
3772
+ throw new Error("CartridgeSuggestion: registryUrl is required and must be a non-empty string");
3773
+ }
3774
+ this.registryUrl = data.registryUrl;
3709
3775
  }
3710
3776
  }
3711
3777
 
3712
3778
  /**
3713
3779
  * Cartridge registry cache entry. The cartridges map is keyed by
3714
- * `<channel>:<id>` so the same id can independently coexist in both
3715
- * channels.
3780
+ * `<registryUrl>:<channel>:<id>` so the same id can independently
3781
+ * coexist across multiple registries × both channels.
3716
3782
  */
3717
3783
  class CartridgeRepoCache {
3718
3784
  constructor(repoUrl) {
3719
- this.cartridges = new Map(); // "<channel>:<id>" -> CartridgeInfo
3720
- this.capToCartridges = new Map(); // cap_urn -> [{channel, id}]
3785
+ this.cartridges = new Map(); // "<registryUrl>:<channel>:<id>" -> CartridgeInfo
3786
+ this.capToCartridges = new Map(); // cap_urn -> [{registryUrl, channel, id}]
3721
3787
  this.lastUpdated = Date.now();
3722
3788
  this.repoUrl = repoUrl;
3723
3789
  }
3724
3790
  }
3725
3791
 
3726
- function _cacheKey(channel, id) {
3727
- return `${channel}:${id}`;
3792
+ function _cacheKey(registryUrl, channel, id) {
3793
+ return `${registryUrl}:${channel}:${id}`;
3728
3794
  }
3729
3795
 
3730
3796
  /**
@@ -3756,6 +3822,21 @@ class CartridgeRepoClient {
3756
3822
  if (data.schemaVersion !== '5.0') {
3757
3823
  throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
3758
3824
  }
3825
+ // Self-referential check: the manifest declares its own URL via
3826
+ // `registryUrl`. It must match the URL we just fetched from
3827
+ // byte-for-byte — a mismatch is a manifest-corruption signal
3828
+ // (publisher wrote the wrong URL, or manifest is being served
3829
+ // from an unexpected mirror). Identity downstream depends on
3830
+ // this string; refuse to ingest on mismatch.
3831
+ if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
3832
+ throw new Error(`Cartridge registry from ${repoUrl}: missing required top-level 'registryUrl' field`);
3833
+ }
3834
+ if (data.registryUrl !== repoUrl) {
3835
+ throw new Error(
3836
+ `Cartridge registry from ${repoUrl}: declared registryUrl '${data.registryUrl}' ` +
3837
+ `does not match the URL it was fetched from. These must match byte-for-byte.`
3838
+ );
3839
+ }
3759
3840
  if (!data.channels || typeof data.channels !== 'object') {
3760
3841
  throw new Error(`Cartridge registry from ${repoUrl}: missing channels object`);
3761
3842
  }
@@ -3771,7 +3852,12 @@ class CartridgeRepoClient {
3771
3852
  ...c,
3772
3853
  id,
3773
3854
  version: c.latestVersion,
3774
- channel
3855
+ channel,
3856
+ // Stamp registryUrl onto every entry — verbatim from the
3857
+ // registry self-reference (which we just verified equals
3858
+ // the fetched URL). Identity comparison downstream is
3859
+ // byte equality.
3860
+ registryUrl: data.registryUrl
3775
3861
  }));
3776
3862
  }
3777
3863
  }
@@ -3794,7 +3880,10 @@ class CartridgeRepoClient {
3794
3880
  const cache = new CartridgeRepoCache(repoUrl);
3795
3881
 
3796
3882
  for (const cartridge of cartridges) {
3797
- cache.cartridges.set(_cacheKey(cartridge.channel, cartridge.id), cartridge);
3883
+ cache.cartridges.set(
3884
+ _cacheKey(cartridge.registryUrl, cartridge.channel, cartridge.id),
3885
+ cartridge
3886
+ );
3798
3887
 
3799
3888
  for (const cap of cartridge.allCaps()) {
3800
3889
  const normalized = CapUrn.fromString(cap.urn).toString();
@@ -3802,6 +3891,7 @@ class CartridgeRepoClient {
3802
3891
  cache.capToCartridges.set(normalized, []);
3803
3892
  }
3804
3893
  cache.capToCartridges.get(normalized).push({
3894
+ registryUrl: cartridge.registryUrl,
3805
3895
  channel: cartridge.channel,
3806
3896
  id: cartridge.id
3807
3897
  });
@@ -3866,7 +3956,7 @@ class CartridgeRepoClient {
3866
3956
  if (!refs) continue;
3867
3957
 
3868
3958
  for (const ref of refs) {
3869
- const cartridge = cache.cartridges.get(_cacheKey(ref.channel, ref.id));
3959
+ const cartridge = cache.cartridges.get(_cacheKey(ref.registryUrl, ref.channel, ref.id));
3870
3960
  if (!cartridge) continue;
3871
3961
 
3872
3962
  const capInfo = cartridge.allCaps().find(c => {
@@ -3891,7 +3981,8 @@ class CartridgeRepoClient {
3891
3981
  latestVersion: cartridge.version,
3892
3982
  repoUrl: cache.repoUrl,
3893
3983
  pageUrl: pageUrl,
3894
- channel: cartridge.channel
3984
+ channel: cartridge.channel,
3985
+ registryUrl: cartridge.registryUrl
3895
3986
  }));
3896
3987
  }
3897
3988
  }
@@ -3928,16 +4019,23 @@ class CartridgeRepoClient {
3928
4019
  }
3929
4020
 
3930
4021
  /**
3931
- * Get cartridge info by `(channel, id)`. Channel is required —
3932
- * the same id can independently exist in both channels with
3933
- * different metadata. Returns `null` when not found.
4022
+ * Get cartridge info by `(registryUrl, channel, id)`. All three
4023
+ * are required — the same id can independently exist across
4024
+ * multiple registries × both channels with different metadata.
4025
+ * Returns `null` when not found. `registryUrl` is the verbatim
4026
+ * URL the cache was indexed under.
3934
4027
  */
3935
- getCartridge(channel, cartridgeId) {
4028
+ getCartridge(registryUrl, channel, cartridgeId) {
4029
+ if (typeof registryUrl !== 'string' || registryUrl.length === 0) {
4030
+ throw new Error('getCartridge: registryUrl must be a non-empty string');
4031
+ }
3936
4032
  if (channel !== 'release' && channel !== 'nightly') {
3937
4033
  throw new Error(`Invalid channel '${channel}' — must be 'release' or 'nightly'`);
3938
4034
  }
3939
- const key = _cacheKey(channel, cartridgeId);
3940
- for (const cache of this.caches.values()) {
4035
+ const key = _cacheKey(registryUrl, channel, cartridgeId);
4036
+ // Cache outer key is also the registry URL, so look up directly.
4037
+ const cache = this.caches.get(registryUrl);
4038
+ if (cache) {
3941
4039
  const cartridge = cache.cartridges.get(key);
3942
4040
  if (cartridge) {
3943
4041
  return cartridge;
@@ -4001,6 +4099,9 @@ class CartridgeRepoServer {
4001
4099
  if (this.registry.schemaVersion !== '5.0') {
4002
4100
  throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
4003
4101
  }
4102
+ if (typeof this.registry.registryUrl !== 'string' || this.registry.registryUrl.length === 0) {
4103
+ throw new Error('Registry must have a non-empty top-level `registryUrl` field (self-referential URL)');
4104
+ }
4004
4105
  const channels = this.registry.channels;
4005
4106
  if (!channels || typeof channels !== 'object') {
4006
4107
  throw new Error('Registry must have a channels object');
@@ -4082,7 +4183,11 @@ class CartridgeRepoServer {
4082
4183
  tags: cartridge.tags,
4083
4184
  versions: cartridge.versions,
4084
4185
  availableVersions,
4085
- channel
4186
+ channel,
4187
+ // Stamp the manifest's self-referential URL onto every entry —
4188
+ // verbatim from the registry. Identity comparison downstream is
4189
+ // byte equality.
4190
+ registryUrl: this.registry.registryUrl
4086
4191
  };
4087
4192
  }
4088
4193
 
@@ -5409,6 +5514,11 @@ module.exports = {
5409
5514
  CartridgeRepoClient,
5410
5515
  CartridgeRepoServer,
5411
5516
  CartridgeChannel,
5517
+ // Registry slug
5518
+ DEV_SLUG,
5519
+ SLUG_HEX_LEN,
5520
+ slugForRegistryUrl,
5521
+ isRegistrySlug,
5412
5522
  // Machine notation
5413
5523
  MachineSyntaxError,
5414
5524
  MachineSyntaxErrorCodes,
package/capdag.test.js CHANGED
@@ -1646,6 +1646,7 @@ function testJS_mediaSpecConstruction() {
1646
1646
  const sampleRegistry = {
1647
1647
  schemaVersion: '5.0',
1648
1648
  lastUpdated: '2026-02-07T16:48:28Z',
1649
+ registryUrl: 'https://test.example/manifest',
1649
1650
  channels: {
1650
1651
  release: { cartridges: {
1651
1652
  pdfcartridge: {
@@ -1785,6 +1786,7 @@ function test320_cartridgeInfoConstruction() {
1785
1786
  const data = {
1786
1787
  id: 'testcartridge',
1787
1788
  name: 'Test Cartridge',
1789
+ registryUrl: 'https://test.example/manifest',
1788
1790
  channel: 'release',
1789
1791
  version: '1.0.0',
1790
1792
  description: 'A test',
@@ -1815,20 +1817,20 @@ function test320_cartridgeInfoConstruction() {
1815
1817
 
1816
1818
  // TEST321: CartridgeInfo.is_signed() returns true when signature is present
1817
1819
  function test321_cartridgeInfoIsSigned() {
1818
- const signed = new CartridgeInfo({id: 'test', channel: 'release', teamId: 'TEAM', signedAt: '2026-01-01', cap_groups: []});
1820
+ const signed = new CartridgeInfo({id: 'test', registryUrl: 'https://test.example/manifest', channel: 'release', teamId: 'TEAM', signedAt: '2026-01-01', cap_groups: []});
1819
1821
  assert(signed.isSigned() === true, 'Cartridge with teamId and signedAt should be signed');
1820
1822
 
1821
- const unsigned1 = new CartridgeInfo({id: 'test', channel: 'release', teamId: '', signedAt: '2026-01-01', cap_groups: []});
1823
+ const unsigned1 = new CartridgeInfo({id: 'test', registryUrl: 'https://test.example/manifest', channel: 'release', teamId: '', signedAt: '2026-01-01', cap_groups: []});
1822
1824
  assert(unsigned1.isSigned() === false, 'Cartridge without teamId should not be signed');
1823
1825
 
1824
- const unsigned2 = new CartridgeInfo({id: 'test', channel: 'release', teamId: 'TEAM', signedAt: '', cap_groups: []});
1826
+ const unsigned2 = new CartridgeInfo({id: 'test', registryUrl: 'https://test.example/manifest', channel: 'release', teamId: 'TEAM', signedAt: '', cap_groups: []});
1825
1827
  assert(unsigned2.isSigned() === false, 'Cartridge without signedAt should not be signed');
1826
1828
  }
1827
1829
 
1828
1830
  // TEST322: CartridgeInfo.build_for_platform() returns the build matching the current platform
1829
1831
  function test322_cartridgeInfoBuildForPlatform() {
1830
1832
  const withBuilds = new CartridgeInfo({
1831
- id: 'test', channel: 'release', version: '1.0.0', cap_groups: [],
1833
+ id: 'test', registryUrl: 'https://test.example/manifest', channel: 'release', version: '1.0.0', cap_groups: [],
1832
1834
  versions: {
1833
1835
  '1.0.0': {
1834
1836
  builds: [
@@ -1854,7 +1856,7 @@ function test322_cartridgeInfoBuildForPlatform() {
1854
1856
  assert(platforms.includes('darwin-arm64'), 'Should include darwin-arm64');
1855
1857
  assert(platforms.includes('linux-x86_64'), 'Should include linux-x86_64');
1856
1858
 
1857
- const noBuilds = new CartridgeInfo({id: 'test', channel: 'release', version: '1.0.0', cap_groups: [], versions: {}, availableVersions: []});
1859
+ const noBuilds = new CartridgeInfo({id: 'test', registryUrl: 'https://test.example/manifest', channel: 'release', version: '1.0.0', cap_groups: [], versions: {}, availableVersions: []});
1858
1860
  assert(noBuilds.buildForPlatform('darwin-arm64') === null, 'Should return null when no versions');
1859
1861
  assert(noBuilds.availablePlatforms().length === 0, 'Should have no platforms');
1860
1862
  }
@@ -1878,7 +1880,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
1878
1880
  // Missing channels object
1879
1881
  threw = false;
1880
1882
  try {
1881
- new CartridgeRepoServer({schemaVersion: '5.0'});
1883
+ new CartridgeRepoServer({schemaVersion: '5.0', registryUrl: 'https://test.example/manifest'});
1882
1884
  } catch (e) {
1883
1885
  threw = true;
1884
1886
  assert(e.message.includes('channels'), 'Should reject missing channels');
@@ -1888,7 +1890,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
1888
1890
  // Missing one of the two required channels
1889
1891
  threw = false;
1890
1892
  try {
1891
- new CartridgeRepoServer({schemaVersion: '5.0', channels: {release: {cartridges: {}}}});
1893
+ new CartridgeRepoServer({schemaVersion: '5.0', registryUrl: 'https://test.example/manifest', channels: {release: {cartridges: {}}}});
1892
1894
  } catch (e) {
1893
1895
  threw = true;
1894
1896
  assert(e.message.includes('nightly'), 'Should require nightly channel');
@@ -2046,13 +2048,24 @@ function test330_cartridgeRepoClientUpdateCache() {
2046
2048
  const server = new CartridgeRepoServer(sampleRegistry);
2047
2049
  const cartridges = server.transformToCartridgeArray().map(p => new CartridgeInfo(p));
2048
2050
 
2049
- client.updateCache('https://example.com/api/cartridges', cartridges);
2051
+ // Cache key is the registry URL the cartridges carry — mismatching
2052
+ // it would orphan the entries (the new cache key is
2053
+ // <registryUrl>:<channel>:<id>). The sampleRegistry stamps every
2054
+ // entry with 'https://test.example/manifest'.
2055
+ const REGISTRY_URL = 'https://test.example/manifest';
2056
+ client.updateCache(REGISTRY_URL, cartridges);
2050
2057
 
2051
- const cache = client.caches.get('https://example.com/api/cartridges');
2058
+ const cache = client.caches.get(REGISTRY_URL);
2052
2059
  assert(cache !== undefined, 'Cache should exist');
2053
2060
  assert(cache.cartridges.size === 3, 'Should have 3 cartridges in cache (2 release + 1 nightly)');
2054
- assert(cache.cartridges.has('release:pdfcartridge'), 'Should key by <channel>:<id>');
2055
- assert(cache.cartridges.has('nightly:jsoncartridge'), 'Should hold nightly entry independently');
2061
+ assert(
2062
+ cache.cartridges.has(`${REGISTRY_URL}:release:pdfcartridge`),
2063
+ 'Should key by <registryUrl>:<channel>:<id>'
2064
+ );
2065
+ assert(
2066
+ cache.cartridges.has(`${REGISTRY_URL}:nightly:jsoncartridge`),
2067
+ 'Should hold nightly entry independently'
2068
+ );
2056
2069
  assert(cache.capToCartridges.size > 0, 'Should have cap mappings');
2057
2070
  }
2058
2071
 
@@ -2093,27 +2106,38 @@ function test332_cartridgeRepoClientGetCartridge() {
2093
2106
  const server = new CartridgeRepoServer(sampleRegistry);
2094
2107
  const cartridges = server.transformToCartridgeArray().map(p => new CartridgeInfo(p));
2095
2108
 
2096
- client.updateCache('https://example.com/api/cartridges', cartridges);
2109
+ const REGISTRY_URL = 'https://test.example/manifest';
2110
+ client.updateCache(REGISTRY_URL, cartridges);
2097
2111
 
2098
- const cartridge = client.getCartridge('release', 'pdfcartridge');
2112
+ const cartridge = client.getCartridge(REGISTRY_URL, 'release', 'pdfcartridge');
2099
2113
  assert(cartridge !== null && cartridge !== undefined, 'Should find cartridge in release');
2100
2114
  assert(cartridge.id === 'pdfcartridge', 'Should have correct ID');
2101
2115
  assert(cartridge.channel === 'release', 'Should report release channel');
2116
+ assert(cartridge.registryUrl === REGISTRY_URL, 'Should report registry URL');
2102
2117
 
2103
- const json = client.getCartridge('nightly', 'jsoncartridge');
2118
+ const json = client.getCartridge(REGISTRY_URL, 'nightly', 'jsoncartridge');
2104
2119
  assert(json !== null && json !== undefined, 'Should find nightly entry');
2105
2120
  assert(json.channel === 'nightly', 'Should report nightly channel');
2106
2121
 
2107
- const wrongChannel = client.getCartridge('nightly', 'pdfcartridge');
2122
+ const wrongChannel = client.getCartridge(REGISTRY_URL, 'nightly', 'pdfcartridge');
2108
2123
  assert(wrongChannel === undefined || wrongChannel === null,
2109
2124
  'Should miss when looking up release id in nightly channel');
2110
2125
 
2111
- const notFound = client.getCartridge('release', 'nonexistent');
2126
+ const notFound = client.getCartridge(REGISTRY_URL, 'release', 'nonexistent');
2112
2127
  assert(notFound === undefined || notFound === null, 'Should miss for unknown id');
2113
2128
 
2129
+ // Two URLs that look similar but differ byte-wise are distinct
2130
+ // registries — looking up one cartridge under the other's URL
2131
+ // misses, even if id+channel match.
2132
+ const wrongRegistry = client.getCartridge(
2133
+ 'https://other.example/manifest', 'release', 'pdfcartridge'
2134
+ );
2135
+ assert(wrongRegistry === undefined || wrongRegistry === null,
2136
+ 'Should miss when looking up under a different registry URL');
2137
+
2114
2138
  let threw = false;
2115
2139
  try {
2116
- client.getCartridge('staging', 'pdfcartridge');
2140
+ client.getCartridge(REGISTRY_URL, 'staging', 'pdfcartridge');
2117
2141
  } catch (e) {
2118
2142
  threw = true;
2119
2143
  }
@@ -2166,13 +2190,15 @@ function test335_cartridgeRepoServerClientIntegration() {
2166
2190
  // Client consumes API response
2167
2191
  const client = new CartridgeRepoClient(3600);
2168
2192
  const cartridges = apiResponse.cartridges.map(p => new CartridgeInfo(p));
2169
- client.updateCache('https://example.com/api/cartridges', cartridges);
2193
+ const REGISTRY_URL = 'https://test.example/manifest';
2194
+ client.updateCache(REGISTRY_URL, cartridges);
2170
2195
 
2171
- // Client can find cartridge by (channel, id)
2172
- const cartridge = client.getCartridge('release', 'pdfcartridge');
2196
+ // Client can find cartridge by (registryUrl, channel, id)
2197
+ const cartridge = client.getCartridge(REGISTRY_URL, 'release', 'pdfcartridge');
2173
2198
  assert(cartridge !== null && cartridge !== undefined, 'Client should find cartridge from server data');
2174
2199
  assert(cartridge.isSigned(), 'Cartridge should be signed');
2175
2200
  assert(cartridge.channel === 'release', 'Should report release channel');
2201
+ assert(cartridge.registryUrl === REGISTRY_URL, 'Should report registry URL');
2176
2202
  assert(cartridge.buildForPlatform('darwin-arm64') !== null, 'Cartridge should have darwin-arm64 build');
2177
2203
 
2178
2204
  // Client can get suggestions
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "pretest": "npm run build:parser",
41
41
  "test": "node capdag.test.js"
42
42
  },
43
- "version": "0.152.345"
43
+ "version": "0.153.347"
44
44
  }