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.
- package/capdag.js +130 -20
- package/capdag.test.js +46 -20
- 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.
|
|
3692
|
-
*
|
|
3693
|
-
* the
|
|
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
|
|
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(
|
|
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)`.
|
|
3932
|
-
* the same id can independently exist
|
|
3933
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
2055
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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