capdag 1.474.0 → 1.480.4
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 +616 -261
- package/formal/index.d.ts +1 -1
- package/formal/index.js +2 -2
- package/formal/program.wasm +0 -0
- package/package.json +3 -3
package/capdag.js
CHANGED
|
@@ -1962,30 +1962,9 @@ function modelPathUrn() {
|
|
|
1962
1962
|
// SCHEMA URL CONFIGURATION
|
|
1963
1963
|
// =============================================================================
|
|
1964
1964
|
|
|
1965
|
+
/** Base URL for capdag schemas. */
|
|
1965
1966
|
const DEFAULT_SCHEMA_BASE = 'https://capdag.com/schema';
|
|
1966
1967
|
|
|
1967
|
-
/**
|
|
1968
|
-
* Get the schema base URL from environment variables or default
|
|
1969
|
-
*
|
|
1970
|
-
* Checks in order:
|
|
1971
|
-
* 1. CDG_SCHEMA_BASE_URL environment variable
|
|
1972
|
-
* 2. CDG_FABRIC_REGISTRY_URL environment variable + "/schema"
|
|
1973
|
-
* 3. Default: "https://capdag.com/schema"
|
|
1974
|
-
*
|
|
1975
|
-
* @returns {string} The schema base URL
|
|
1976
|
-
*/
|
|
1977
|
-
function getSchemaBaseURL() {
|
|
1978
|
-
if (typeof process !== 'undefined' && process.env) {
|
|
1979
|
-
if (process.env.CDG_SCHEMA_BASE_URL) {
|
|
1980
|
-
return process.env.CDG_SCHEMA_BASE_URL;
|
|
1981
|
-
}
|
|
1982
|
-
if (process.env.CDG_FABRIC_REGISTRY_URL) {
|
|
1983
|
-
return process.env.CDG_FABRIC_REGISTRY_URL + '/schema';
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
return DEFAULT_SCHEMA_BASE;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
1968
|
/**
|
|
1990
1969
|
* Get a profile URL for the given profile name
|
|
1991
1970
|
*
|
|
@@ -1993,7 +1972,7 @@ function getSchemaBaseURL() {
|
|
|
1993
1972
|
* @returns {string} The full profile URL
|
|
1994
1973
|
*/
|
|
1995
1974
|
function getProfileURL(profileName) {
|
|
1996
|
-
return `${
|
|
1975
|
+
return `${DEFAULT_SCHEMA_BASE}/${profileName}`;
|
|
1997
1976
|
}
|
|
1998
1977
|
|
|
1999
1978
|
/**
|
|
@@ -5209,69 +5188,408 @@ class CartridgeCapGroup {
|
|
|
5209
5188
|
}
|
|
5210
5189
|
|
|
5211
5190
|
// =============================================================================
|
|
5212
|
-
//
|
|
5191
|
+
// Registry charters
|
|
5213
5192
|
// =============================================================================
|
|
5214
5193
|
//
|
|
5215
|
-
//
|
|
5216
|
-
//
|
|
5217
|
-
//
|
|
5218
|
-
//
|
|
5219
|
-
//
|
|
5194
|
+
// What makes a URL a MachineFabric registry. Mirrors Rust
|
|
5195
|
+
// `bifaci/charter.rs`.
|
|
5196
|
+
//
|
|
5197
|
+
// A registry — of cartridges, of fabric definitions, or of app releases — is
|
|
5198
|
+
// named by its ANCHOR, which may be any URL. The one convention anywhere is
|
|
5199
|
+
// that `<anchor>/charter.json` and `<anchor>/charter.json.sig` exist.
|
|
5200
|
+
// Everything else is read from the charter, never derived from the shape of a
|
|
5201
|
+
// URL: the registry's slug (its folder on disk), its kind, the environment its
|
|
5202
|
+
// contents are signed under, the locations of its manifests, and for a
|
|
5203
|
+
// cartridge registry the fabric its caps resolve against.
|
|
5220
5204
|
//
|
|
5221
|
-
//
|
|
5222
|
-
//
|
|
5223
|
-
//
|
|
5205
|
+
// This reads a charter and holds it to its shape, to the folder it is stored
|
|
5206
|
+
// in and to the anchor its cartridges record. It does NOT verify the
|
|
5207
|
+
// signature: this mirror carries no chain verification (`release_cert.rs` and
|
|
5208
|
+
// `charter.rs` in the Rust library are the only implementation of it), and a
|
|
5209
|
+
// mirror that stubbed one would be worse than a mirror that has none. The
|
|
5210
|
+
// browser reads slugs only from charters it is given; it derives none.
|
|
5211
|
+
|
|
5212
|
+
/** The format discriminator inside every charter body. */
|
|
5213
|
+
const CHARTER_FORMAT = 'machinefabric-registry-charter/1';
|
|
5214
|
+
/** The charter's file name under its anchor and in a registry's folder. */
|
|
5215
|
+
const CHARTER_FILE = 'charter.json';
|
|
5216
|
+
/** The charter signature's file name. */
|
|
5217
|
+
const CHARTER_SIG_FILE = 'charter.json.sig';
|
|
5218
|
+
/**
|
|
5219
|
+
* The reserved folder name of cartridges built with no registry. No charter
|
|
5220
|
+
* may claim it.
|
|
5221
|
+
*/
|
|
5222
|
+
const DEV_SLUG = 'dev';
|
|
5223
|
+
|
|
5224
|
+
/** The three kinds of registry. */
|
|
5225
|
+
const RegistryKind = Object.freeze({
|
|
5226
|
+
CARTRIDGES: 'cartridges',
|
|
5227
|
+
FABRIC: 'fabric',
|
|
5228
|
+
RELEASES: 'releases',
|
|
5229
|
+
});
|
|
5224
5230
|
|
|
5225
|
-
|
|
5231
|
+
/**
|
|
5232
|
+
* Per kind, the locations a charter must declare, each with the placeholders
|
|
5233
|
+
* its template must use — all of them and no others.
|
|
5234
|
+
*/
|
|
5235
|
+
const REGISTRY_LOCATIONS = Object.freeze({
|
|
5236
|
+
[RegistryKind.CARTRIDGES]: { manifest: ['registry_version'] },
|
|
5237
|
+
[RegistryKind.FABRIC]: { manifest: ['manifest_version'], latest: [], definition: ['kind', 'urn_hash', 'version'] },
|
|
5238
|
+
[RegistryKind.RELEASES]: { manifest: ['app_id'] },
|
|
5239
|
+
});
|
|
5240
|
+
|
|
5241
|
+
const CHARTER_FIELDS = ['format', 'anchor', 'kind', 'slug', 'environment', 'experiment', 'locations', 'fabric', 'issued_at', 'not_after'];
|
|
5242
|
+
const CHARTER_REQUIRED_FIELDS = ['format', 'anchor', 'kind', 'slug', 'environment', 'locations', 'issued_at', 'not_after'];
|
|
5243
|
+
|
|
5244
|
+
/** Why a charter could not be had or used. */
|
|
5245
|
+
const CharterErrorKind = Object.freeze({
|
|
5246
|
+
/** The bytes are not a charter document, or it breaks its shape. */
|
|
5247
|
+
MALFORMED: 'malformed',
|
|
5248
|
+
/** A charter that reads, and is not the one asked for. */
|
|
5249
|
+
REFUSED: 'refused',
|
|
5250
|
+
/** A stored charter could not be read from disk. */
|
|
5251
|
+
UNREADABLE: 'unreadable',
|
|
5252
|
+
/** Nothing is at `<anchor>/charter.json`: the URL is not a MachineFabric registry. */
|
|
5253
|
+
UNCHARTERED: 'unchartered',
|
|
5254
|
+
/** The anchor answered with an HTTP error other than 404. */
|
|
5255
|
+
HTTP: 'http',
|
|
5256
|
+
/** The anchor could not be reached. */
|
|
5257
|
+
UNREACHABLE: 'unreachable',
|
|
5258
|
+
});
|
|
5259
|
+
|
|
5260
|
+
class CharterError extends Error {
|
|
5261
|
+
constructor(kind, message) {
|
|
5262
|
+
super(message);
|
|
5263
|
+
this.name = 'CharterError';
|
|
5264
|
+
this.kind = kind;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
const malformedCharter = (message) => new CharterError(CharterErrorKind.MALFORMED, message);
|
|
5269
|
+
|
|
5270
|
+
/** Whether `s` can be a registry slug: `[a-z0-9][a-z0-9.-]{0,62}`, and not the reserved "dev". */
|
|
5271
|
+
function isRegistrySlug(s) {
|
|
5272
|
+
return typeof s === 'string' && s !== DEV_SLUG && /^[a-z0-9][a-z0-9.-]{0,62}$/.test(s);
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
/** Whether `s` can be a lab experiment id: `[a-z0-9][a-z0-9-]{0,39}`. */
|
|
5276
|
+
function isExperimentId(s) {
|
|
5277
|
+
return typeof s === 'string' && /^[a-z0-9][a-z0-9-]{0,39}$/.test(s);
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
function checkHttps(what, url) {
|
|
5281
|
+
const i = url.indexOf('://');
|
|
5282
|
+
const rest = i >= 0 ? url.slice(i + 3) : '';
|
|
5283
|
+
if (i < 0 || url.slice(0, i) !== 'https' || rest.length === 0 || rest.startsWith('/')) {
|
|
5284
|
+
throw malformedCharter(`${what} '${url}' is not an https URL`);
|
|
5285
|
+
}
|
|
5286
|
+
if (/[?#]/.test(url)) {
|
|
5287
|
+
throw malformedCharter(`${what} '${url}' carries a query or fragment`);
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5226
5290
|
|
|
5227
5291
|
/**
|
|
5228
|
-
* The
|
|
5229
|
-
*
|
|
5292
|
+
* The {name} placeholders a template uses, in order of first use. A stray
|
|
5293
|
+
* brace is an error rather than literal text.
|
|
5230
5294
|
*/
|
|
5231
|
-
function
|
|
5232
|
-
const
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5295
|
+
function placeholdersIn(template) {
|
|
5296
|
+
const out = [];
|
|
5297
|
+
let rest = template;
|
|
5298
|
+
for (;;) {
|
|
5299
|
+
const at = rest.search(/[{}]/);
|
|
5300
|
+
if (at < 0) return out;
|
|
5301
|
+
if (rest[at] === '}') throw malformedCharter(`'${template}' has an unmatched '}'`);
|
|
5302
|
+
const after = rest.slice(at + 1);
|
|
5303
|
+
const end = after.indexOf('}');
|
|
5304
|
+
if (end < 0) throw malformedCharter(`'${template}' has an unclosed '{'`);
|
|
5305
|
+
const name = after.slice(0, end);
|
|
5306
|
+
if (!/^[a-z_]+$/.test(name)) {
|
|
5307
|
+
throw malformedCharter(`'${template}' has a placeholder '{${name}}' that is not a name`);
|
|
5308
|
+
}
|
|
5309
|
+
if (!out.includes(name)) out.push(name);
|
|
5310
|
+
rest = after.slice(end + 1);
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
const isTime = (value) => Number.isInteger(value) && value >= 0;
|
|
5315
|
+
|
|
5316
|
+
/** A charter body, as signed. */
|
|
5317
|
+
class Charter {
|
|
5318
|
+
/**
|
|
5319
|
+
* A charter's fields, each of the type a charter has; no field a charter
|
|
5320
|
+
* does not have. The shape rules are `validate`'s.
|
|
5321
|
+
* @param {Object} obj
|
|
5322
|
+
* @returns {Charter}
|
|
5323
|
+
*/
|
|
5324
|
+
static fromObject(obj) {
|
|
5325
|
+
const notACharter = (why) => malformedCharter(`the charter is not a charter document: ${why}`);
|
|
5326
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) throw notACharter('not a JSON object');
|
|
5327
|
+
for (const key of Object.keys(obj).sort()) {
|
|
5328
|
+
if (!CHARTER_FIELDS.includes(key)) throw notACharter(`unknown field '${key}'`);
|
|
5329
|
+
}
|
|
5330
|
+
for (const key of CHARTER_REQUIRED_FIELDS) {
|
|
5331
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) throw notACharter(`no '${key}'`);
|
|
5332
|
+
}
|
|
5333
|
+
for (const key of ['format', 'anchor', 'kind', 'slug', 'environment']) {
|
|
5334
|
+
if (typeof obj[key] !== 'string') throw notACharter(`'${key}' is not a string`);
|
|
5335
|
+
}
|
|
5336
|
+
for (const key of ['experiment', 'fabric']) {
|
|
5337
|
+
if (obj[key] !== undefined && obj[key] !== null && typeof obj[key] !== 'string') {
|
|
5338
|
+
throw notACharter(`'${key}' is not a string`);
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
if (!Object.values(RegistryKind).includes(obj.kind)) throw notACharter(`kind '${obj.kind}' is not a kind of registry`);
|
|
5342
|
+
const locations = obj.locations;
|
|
5343
|
+
if (locations === null || typeof locations !== 'object' || Array.isArray(locations)
|
|
5344
|
+
|| !Object.values(locations).every(v => typeof v === 'string')) {
|
|
5345
|
+
throw notACharter('locations are not templates by name');
|
|
5346
|
+
}
|
|
5347
|
+
if (!isTime(obj.issued_at)) throw notACharter(`'issued_at' is not a time`);
|
|
5348
|
+
if (obj.not_after !== null && !isTime(obj.not_after)) throw notACharter(`'not_after' is not a time`);
|
|
5349
|
+
const charter = new Charter();
|
|
5350
|
+
charter.format = obj.format;
|
|
5351
|
+
charter.anchor = obj.anchor;
|
|
5352
|
+
charter.kind = obj.kind;
|
|
5353
|
+
charter.slug = obj.slug;
|
|
5354
|
+
charter.environment = obj.environment;
|
|
5355
|
+
charter.experiment = obj.experiment === undefined ? null : obj.experiment;
|
|
5356
|
+
charter.locations = { ...locations };
|
|
5357
|
+
charter.fabric = obj.fabric === undefined ? null : obj.fabric;
|
|
5358
|
+
charter.issuedAt = obj.issued_at;
|
|
5359
|
+
charter.notAfter = obj.not_after;
|
|
5360
|
+
return charter;
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
/**
|
|
5364
|
+
* A charter's JSON, read: the format first, then the fields, then the shape.
|
|
5365
|
+
* @param {Object} obj - The parsed JSON body
|
|
5366
|
+
* @returns {Charter}
|
|
5367
|
+
*/
|
|
5368
|
+
static parse(obj) {
|
|
5369
|
+
if (obj === null || typeof obj !== 'object' || obj.format === undefined) {
|
|
5370
|
+
throw malformedCharter('the charter is not a charter document: no format');
|
|
5371
|
+
}
|
|
5372
|
+
if (obj.format !== CHARTER_FORMAT) {
|
|
5373
|
+
throw malformedCharter(`the charter declares format '${obj.format}' (this build reads '${CHARTER_FORMAT}')`);
|
|
5374
|
+
}
|
|
5375
|
+
const charter = Charter.fromObject(obj);
|
|
5376
|
+
try {
|
|
5377
|
+
charter.validate();
|
|
5378
|
+
} catch (e) {
|
|
5379
|
+
throw malformedCharter(`the charter is not a charter document: ${e.message}`);
|
|
5380
|
+
}
|
|
5381
|
+
return charter;
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
/** The charter as its JSON object. */
|
|
5385
|
+
toObject() {
|
|
5386
|
+
const out = {
|
|
5387
|
+
format: this.format, anchor: this.anchor, kind: this.kind, slug: this.slug, environment: this.environment,
|
|
5388
|
+
};
|
|
5389
|
+
if (this.experiment !== null) out.experiment = this.experiment;
|
|
5390
|
+
out.locations = { ...this.locations };
|
|
5391
|
+
if (this.fabric !== null) out.fabric = this.fabric;
|
|
5392
|
+
out.issued_at = this.issuedAt;
|
|
5393
|
+
out.not_after = this.notAfter;
|
|
5394
|
+
return out;
|
|
5395
|
+
}
|
|
5396
|
+
|
|
5397
|
+
/** Hold the charter to the shape every charter must have, whoever signed it. */
|
|
5398
|
+
validate() {
|
|
5399
|
+
if (this.format !== CHARTER_FORMAT) throw malformedCharter(`format '${this.format}' is not '${CHARTER_FORMAT}'`);
|
|
5400
|
+
checkHttps('anchor', this.anchor);
|
|
5401
|
+
if (this.anchor.endsWith('/')) throw malformedCharter(`anchor '${this.anchor}' ends with '/'`);
|
|
5402
|
+
if (!isRegistrySlug(this.slug)) {
|
|
5403
|
+
throw malformedCharter(`slug '${this.slug}' is not a registry slug ([a-z0-9][a-z0-9.-]{0,62}, not '${DEV_SLUG}')`);
|
|
5404
|
+
}
|
|
5405
|
+
if (!/^[a-z0-9-]+$/.test(this.environment)) {
|
|
5406
|
+
throw malformedCharter(`environment '${this.environment}' is not a label ([a-z0-9-]+)`);
|
|
5407
|
+
}
|
|
5408
|
+
if (this.experiment !== null && !isExperimentId(this.experiment)) {
|
|
5409
|
+
throw malformedCharter(`experiment '${this.experiment}' is not an experiment id ([a-z0-9][a-z0-9-]{0,39})`);
|
|
5410
|
+
}
|
|
5411
|
+
const required = REGISTRY_LOCATIONS[this.kind];
|
|
5412
|
+
for (const name of Object.keys(this.locations)) {
|
|
5413
|
+
if (!(name in required)) throw malformedCharter(`location '${name}' is not one a ${this.kind} registry has`);
|
|
5414
|
+
}
|
|
5415
|
+
for (const name of Object.keys(required).sort()) {
|
|
5416
|
+
const template = this.locations[name];
|
|
5417
|
+
if (template === undefined) throw malformedCharter(`a ${this.kind} registry must declare location '${name}'`);
|
|
5418
|
+
checkHttps(`location '${name}'`, template);
|
|
5419
|
+
const used = placeholdersIn(template);
|
|
5420
|
+
for (const placeholder of used) {
|
|
5421
|
+
if (!required[name].includes(placeholder)) {
|
|
5422
|
+
throw malformedCharter(`location '${name}' uses '{${placeholder}}', which it does not take`);
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
for (const placeholder of required[name]) {
|
|
5426
|
+
if (!used.includes(placeholder)) throw malformedCharter(`location '${name}' must use '{${placeholder}}'`);
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
if (this.kind === RegistryKind.CARTRIDGES) {
|
|
5430
|
+
if (this.fabric === null) throw malformedCharter('a cartridges registry must name its fabric');
|
|
5431
|
+
checkHttps('fabric', this.fabric);
|
|
5432
|
+
if (this.fabric.endsWith('/')) throw malformedCharter(`fabric '${this.fabric}' ends with '/'`);
|
|
5433
|
+
} else if (this.fabric !== null) {
|
|
5434
|
+
throw malformedCharter(`only a cartridges registry names a fabric, and this is a ${this.kind} registry`);
|
|
5435
|
+
}
|
|
5436
|
+
if (this.notAfter !== null && this.notAfter <= this.issuedAt) {
|
|
5437
|
+
throw malformedCharter(`not_after ${this.notAfter} is not after issued_at ${this.issuedAt}`);
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
/**
|
|
5442
|
+
* One location with its placeholders filled. Every placeholder the template
|
|
5443
|
+
* uses must be given, and nothing else may be.
|
|
5444
|
+
* @param {string} name
|
|
5445
|
+
* @param {Object<string, string|number>} values
|
|
5446
|
+
* @returns {string}
|
|
5447
|
+
*/
|
|
5448
|
+
location(name, values) {
|
|
5449
|
+
const template = this.locations[name];
|
|
5450
|
+
if (template === undefined) {
|
|
5451
|
+
throw malformedCharter(`the ${this.kind} registry at ${this.anchor} has no location '${name}'`);
|
|
5452
|
+
}
|
|
5453
|
+
const used = placeholdersIn(template);
|
|
5454
|
+
for (const key of Object.keys(values)) {
|
|
5455
|
+
if (!used.includes(key)) throw malformedCharter(`location '${name}' does not take '${key}'`);
|
|
5456
|
+
}
|
|
5457
|
+
let out = template;
|
|
5458
|
+
for (const placeholder of used) {
|
|
5459
|
+
if (!(placeholder in values)) throw malformedCharter(`location '${name}' needs '${placeholder}'`);
|
|
5460
|
+
out = out.split(`{${placeholder}}`).join(String(values[placeholder]));
|
|
5461
|
+
}
|
|
5462
|
+
return out;
|
|
5463
|
+
}
|
|
5236
5464
|
}
|
|
5237
5465
|
|
|
5238
5466
|
/**
|
|
5239
|
-
*
|
|
5240
|
-
*
|
|
5241
|
-
*
|
|
5242
|
-
* ASCII-lowercased, every character outside `[a-z0-9.-]` replaced by `-`.
|
|
5243
|
-
* Depends ONLY on the authority; path/version/query/trailing-slash/host-case
|
|
5244
|
-
* do not change it.
|
|
5467
|
+
* The charter at `<anchor>/charter.json`, read and held to the anchor and kind
|
|
5468
|
+
* asked. The signature beside it is the Rust host's to verify (see above); a
|
|
5469
|
+
* client of this mirror reads the charter's locations and nothing more.
|
|
5245
5470
|
*
|
|
5246
|
-
* @param {string
|
|
5247
|
-
* @
|
|
5471
|
+
* @param {string} anchor
|
|
5472
|
+
* @param {string} kind - A RegistryKind
|
|
5473
|
+
* @param {function(string): Promise<{status: number, ok: boolean, json: function(): Promise<*>}>} fetchIt
|
|
5474
|
+
* @returns {Promise<Charter>}
|
|
5475
|
+
*/
|
|
5476
|
+
async function fetchCharter(anchor, kind, fetchIt) {
|
|
5477
|
+
const url = `${anchor}/${CHARTER_FILE}`;
|
|
5478
|
+
let response;
|
|
5479
|
+
try {
|
|
5480
|
+
response = await fetchIt(`${url}?cb=${Date.now()}`);
|
|
5481
|
+
} catch (e) {
|
|
5482
|
+
throw new CharterError(CharterErrorKind.UNREACHABLE, `cannot reach ${url}: ${e.message}`);
|
|
5483
|
+
}
|
|
5484
|
+
if (response.status === 404) {
|
|
5485
|
+
throw new CharterError(CharterErrorKind.UNCHARTERED, `${anchor} has no charter (${url} answered 404) — it is not a MachineFabric registry`);
|
|
5486
|
+
}
|
|
5487
|
+
if (!response.ok) {
|
|
5488
|
+
throw new CharterError(CharterErrorKind.HTTP, `${url} answered HTTP ${response.status}`);
|
|
5489
|
+
}
|
|
5490
|
+
const refused = (why) => new CharterError(CharterErrorKind.REFUSED, `the charter of ${anchor} was refused: ${why}`);
|
|
5491
|
+
let charter;
|
|
5492
|
+
try {
|
|
5493
|
+
charter = Charter.parse(await response.json());
|
|
5494
|
+
} catch (e) {
|
|
5495
|
+
throw refused(e.message);
|
|
5496
|
+
}
|
|
5497
|
+
if (charter.anchor !== anchor) throw refused(`it is the charter of ${charter.anchor}`);
|
|
5498
|
+
if (charter.kind !== kind) throw refused(`it charters a ${charter.kind} registry, not a ${kind} one`);
|
|
5499
|
+
return charter;
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
/**
|
|
5503
|
+
* The charter stored in a registry's folder, held to its shape and its kind,
|
|
5504
|
+
* and the folder to its slug. Node only.
|
|
5505
|
+
* @param {string} folder
|
|
5506
|
+
* @param {string} kind - A RegistryKind
|
|
5507
|
+
* @returns {Charter}
|
|
5248
5508
|
*/
|
|
5249
|
-
function
|
|
5250
|
-
|
|
5251
|
-
|
|
5509
|
+
function readStoredCharter(folder, kind) {
|
|
5510
|
+
const { fs, path } = nodeModules('reading a stored charter');
|
|
5511
|
+
const file = path.join(folder, CHARTER_FILE);
|
|
5512
|
+
let body;
|
|
5513
|
+
try {
|
|
5514
|
+
body = fs.readFileSync(file, 'utf8');
|
|
5515
|
+
} catch (e) {
|
|
5516
|
+
throw new CharterError(CharterErrorKind.UNREADABLE, `cannot read ${file}: ${e.message}`);
|
|
5252
5517
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
const lc = ch >= "A" && ch <= "Z" ? ch.toLowerCase() : ch;
|
|
5256
|
-
out += (lc >= "a" && lc <= "z") || (lc >= "0" && lc <= "9") || lc === "." || lc === "-" ? lc : "-";
|
|
5518
|
+
if (!fs.existsSync(path.join(folder, CHARTER_SIG_FILE))) {
|
|
5519
|
+
throw new CharterError(CharterErrorKind.UNREADABLE, `the charter in ${folder} has no signature beside it`);
|
|
5257
5520
|
}
|
|
5258
|
-
|
|
5521
|
+
let json;
|
|
5522
|
+
try {
|
|
5523
|
+
json = JSON.parse(body);
|
|
5524
|
+
} catch (e) {
|
|
5525
|
+
throw malformedCharter(`the charter is not a charter document: ${e.message}`);
|
|
5526
|
+
}
|
|
5527
|
+
const charter = Charter.parse(json);
|
|
5528
|
+
if (charter.kind !== kind) {
|
|
5529
|
+
throw new CharterError(CharterErrorKind.REFUSED, `the charter is for a ${charter.kind} registry, not a ${kind} one`);
|
|
5530
|
+
}
|
|
5531
|
+
if (path.basename(folder) !== charter.slug) {
|
|
5532
|
+
throw new CharterError(CharterErrorKind.REFUSED,
|
|
5533
|
+
`the charter for '${charter.anchor}' names slug '${charter.slug}', but it is stored in a folder named '${path.basename(folder)}'`);
|
|
5534
|
+
}
|
|
5535
|
+
return charter;
|
|
5259
5536
|
}
|
|
5260
5537
|
|
|
5261
5538
|
/**
|
|
5262
|
-
*
|
|
5263
|
-
*
|
|
5264
|
-
*
|
|
5539
|
+
* Among the registry folders under `root`, the one whose stored charter is of
|
|
5540
|
+
* `kind` and for `anchor`: `{folder, charter}`, or null. A folder whose
|
|
5541
|
+
* charter does not read is passed over. Node only.
|
|
5265
5542
|
*/
|
|
5266
|
-
|
|
5267
|
-
|
|
5543
|
+
function storedCharterFor(root, anchor, kind) {
|
|
5544
|
+
const { fs, path } = nodeModules('finding a stored charter');
|
|
5545
|
+
let names;
|
|
5546
|
+
try {
|
|
5547
|
+
names = fs.readdirSync(root).sort();
|
|
5548
|
+
} catch (_e) {
|
|
5549
|
+
return null;
|
|
5550
|
+
}
|
|
5551
|
+
for (const name of names) {
|
|
5552
|
+
const folder = path.join(root, name);
|
|
5553
|
+
if (!fs.existsSync(path.join(folder, CHARTER_FILE))) continue;
|
|
5554
|
+
let charter;
|
|
5555
|
+
try {
|
|
5556
|
+
charter = readStoredCharter(folder, kind);
|
|
5557
|
+
} catch (e) {
|
|
5558
|
+
console.warn(`[capdag] the charter stored in ${folder} is not used: ${e.message}`);
|
|
5559
|
+
continue;
|
|
5560
|
+
}
|
|
5561
|
+
if (charter.anchor === anchor) return { folder, charter };
|
|
5562
|
+
}
|
|
5563
|
+
return null;
|
|
5268
5564
|
}
|
|
5269
5565
|
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5566
|
+
/**
|
|
5567
|
+
* What one top-level folder of a cartridge tree holds: the cartridges built
|
|
5568
|
+
* with no registry, or a chartered registry's — named by the charter stored
|
|
5569
|
+
* in it, never by its own name.
|
|
5570
|
+
*/
|
|
5571
|
+
class RegistryFolder {
|
|
5572
|
+
/** @param {Charter|null} charter - null for the reserved dev folder */
|
|
5573
|
+
constructor(charter) {
|
|
5574
|
+
this.charter = charter;
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
/** Open a top-level folder: "dev" by its reserved name, any other by the cartridges charter stored in it. Node only. */
|
|
5578
|
+
static open(folder) {
|
|
5579
|
+
const { path } = nodeModules('opening a registry folder');
|
|
5580
|
+
if (path.basename(folder) === DEV_SLUG) return new RegistryFolder(null);
|
|
5581
|
+
return new RegistryFolder(readStoredCharter(folder, RegistryKind.CARTRIDGES));
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5584
|
+
/** The folder's name. */
|
|
5585
|
+
get slug() {
|
|
5586
|
+
return this.charter === null ? DEV_SLUG : this.charter.slug;
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
/** The anchor every cartridge in the folder must record: null for dev. */
|
|
5590
|
+
get anchor() {
|
|
5591
|
+
return this.charter === null ? null : this.charter.anchor;
|
|
5592
|
+
}
|
|
5275
5593
|
}
|
|
5276
5594
|
|
|
5277
5595
|
// =============================================================================
|
|
@@ -5590,20 +5908,19 @@ class CartridgeSuggestion {
|
|
|
5590
5908
|
*/
|
|
5591
5909
|
/**
|
|
5592
5910
|
* The schema revision of the cartridge registry this build reads
|
|
5593
|
-
* (`schemas/cartridge-registry/
|
|
5911
|
+
* (`schemas/cartridge-registry/v8.json`): its cap groups carry each
|
|
5594
5912
|
* cartridge's cap definitions exactly as its manifest declares them,
|
|
5595
5913
|
* including the dims each was built from.
|
|
5596
5914
|
*/
|
|
5597
|
-
const CARTRIDGE_REGISTRY_SCHEMA_VERSION = '
|
|
5915
|
+
const CARTRIDGE_REGISTRY_SCHEMA_VERSION = '8.0';
|
|
5598
5916
|
|
|
5599
5917
|
/**
|
|
5600
5918
|
* The cartridge registry regime version this build speaks
|
|
5601
|
-
* (`schemas/cartridge-registry/registry-version.txt`)
|
|
5602
|
-
*
|
|
5603
|
-
*
|
|
5604
|
-
* reinterpreted.
|
|
5919
|
+
* (`schemas/cartridge-registry/registry-version.txt`): the
|
|
5920
|
+
* `{registry_version}` a registry charter's manifest location is filled
|
|
5921
|
+
* with. A registry of any other version is refused, never reinterpreted.
|
|
5605
5922
|
*/
|
|
5606
|
-
const CARTRIDGE_REGISTRY_VERSION =
|
|
5923
|
+
const CARTRIDGE_REGISTRY_VERSION = 4;
|
|
5607
5924
|
|
|
5608
5925
|
class CartridgeRepoCache {
|
|
5609
5926
|
constructor(repoUrl) {
|
|
@@ -5622,18 +5939,53 @@ function _cacheKey(registryUrl, channel, id) {
|
|
|
5622
5939
|
* Cartridge repository client - fetches and caches cartridge registry
|
|
5623
5940
|
*/
|
|
5624
5941
|
class CartridgeRepoClient {
|
|
5625
|
-
|
|
5942
|
+
/**
|
|
5943
|
+
* @param {number} [cacheTtlSeconds]
|
|
5944
|
+
* @param {Object} [options]
|
|
5945
|
+
* @param {function(string): Promise<Response>} [options.fetch] - How a URL
|
|
5946
|
+
* is fetched (the runtime's `fetch` when not given)
|
|
5947
|
+
*/
|
|
5948
|
+
constructor(cacheTtlSeconds = 3600, options = {}) {
|
|
5626
5949
|
this.caches = new Map(); // repo_url -> CartridgeRepoCache
|
|
5627
5950
|
this.cacheTtl = cacheTtlSeconds * 1000; // Convert to milliseconds
|
|
5951
|
+
this._fetch = options.fetch || ((url) => globalThis.fetch(url));
|
|
5952
|
+
// The charter of every registry synced, by anchor.
|
|
5953
|
+
this._charters = new Map();
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
/** The charter of a registry this client has synced, by anchor, or null. */
|
|
5957
|
+
charter(anchor) {
|
|
5958
|
+
return this._charters.get(anchor) || null;
|
|
5628
5959
|
}
|
|
5629
5960
|
|
|
5630
5961
|
/**
|
|
5631
|
-
* Fetch a channel-partitioned
|
|
5632
|
-
* CARTRIDGE_REGISTRY_SCHEMA_VERSION) from
|
|
5633
|
-
* to a list of `CartridgeInfo`, one per
|
|
5962
|
+
* Fetch a registry's charter from its anchor, then the channel-partitioned
|
|
5963
|
+
* manifest (schema CARTRIDGE_REGISTRY_SCHEMA_VERSION) from where the
|
|
5964
|
+
* charter says it is, and flatten it to a list of `CartridgeInfo`, one per
|
|
5965
|
+
* `(channel, id)` pair.
|
|
5966
|
+
*
|
|
5967
|
+
* The charter comes first and decides everything after it: a URL with no
|
|
5968
|
+
* charter, or one whose charter is not this registry's, is never asked for
|
|
5969
|
+
* a manifest. The manifest must name the anchor as its registry and the
|
|
5970
|
+
* fabric the charter names as its fabric.
|
|
5634
5971
|
*/
|
|
5635
5972
|
async fetchRegistry(repoUrl) {
|
|
5636
|
-
const
|
|
5973
|
+
const charter = await fetchCharter(repoUrl, RegistryKind.CARTRIDGES, this._fetch);
|
|
5974
|
+
const manifestUrl = charter.location('manifest', { registry_version: CARTRIDGE_REGISTRY_VERSION });
|
|
5975
|
+
// A SLUG IS ONE REGISTRY'S: charters are unique by slug where they are
|
|
5976
|
+
// issued, so a second anchor whose charter names a slug another
|
|
5977
|
+
// registry's charter already holds is an invalid state, and is refused
|
|
5978
|
+
// rather than given that registry's folder.
|
|
5979
|
+
for (const [heldAnchor, held] of this._charters) {
|
|
5980
|
+
if (heldAnchor !== repoUrl && held.slug === charter.slug) {
|
|
5981
|
+
throw new CharterError(CharterErrorKind.REFUSED,
|
|
5982
|
+
`the charter of ${repoUrl} names the slug '${charter.slug}', which the charter of ${heldAnchor} already holds`);
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
this._charters.set(repoUrl, charter);
|
|
5986
|
+
// The manifest is mutable; a CDN may hold an older publish. A fresh
|
|
5987
|
+
// cache-bust token forces one origin read per sync.
|
|
5988
|
+
const response = await this._fetch(`${manifestUrl}?cb=${Date.now()}`);
|
|
5637
5989
|
if (response.status === 404) {
|
|
5638
5990
|
// Manifest not published yet — return an empty list so the
|
|
5639
5991
|
// caller's cache reflects "no cartridges available" without
|
|
@@ -5653,25 +6005,23 @@ class CartridgeRepoClient {
|
|
|
5653
6005
|
if (data.registryVersion !== CARTRIDGE_REGISTRY_VERSION) {
|
|
5654
6006
|
throw new Error(`Cartridge registry from ${repoUrl} has registryVersion '${data.registryVersion}'; this build speaks v${CARTRIDGE_REGISTRY_VERSION}`);
|
|
5655
6007
|
}
|
|
5656
|
-
//
|
|
5657
|
-
//
|
|
5658
|
-
//
|
|
5659
|
-
|
|
5660
|
-
throw new Error(`Cartridge registry from ${repoUrl}: missing required 'fabricRegistryUrl' (the fabric its caps resolve against)`);
|
|
5661
|
-
}
|
|
5662
|
-
// Self-referential check: the manifest declares its own URL via
|
|
5663
|
-
// `registryUrl`. It must match the URL we just fetched from
|
|
5664
|
-
// byte-for-byte — a mismatch is a manifest-corruption signal
|
|
5665
|
-
// (publisher wrote the wrong URL, or manifest is being served
|
|
5666
|
-
// from an unexpected mirror). Identity downstream depends on
|
|
5667
|
-
// this string; refuse to ingest on mismatch.
|
|
6008
|
+
// The manifest names its registry by anchor, and its fabric as the
|
|
6009
|
+
// charter does — byte-for-byte. A mismatch is a manifest published for
|
|
6010
|
+
// another registry, or served under the wrong one; identity downstream
|
|
6011
|
+
// depends on these strings, so nothing of it is ingested.
|
|
5668
6012
|
if (typeof data.registryUrl !== 'string' || data.registryUrl.length === 0) {
|
|
5669
6013
|
throw new Error(`Cartridge registry from ${repoUrl}: missing required top-level 'registryUrl' field`);
|
|
5670
6014
|
}
|
|
5671
6015
|
if (data.registryUrl !== repoUrl) {
|
|
5672
6016
|
throw new Error(
|
|
5673
6017
|
`Cartridge registry from ${repoUrl}: declared registryUrl '${data.registryUrl}' ` +
|
|
5674
|
-
`
|
|
6018
|
+
`is not the registry's anchor. These must match byte-for-byte.`
|
|
6019
|
+
);
|
|
6020
|
+
}
|
|
6021
|
+
if (data.fabricRegistryUrl !== charter.fabric) {
|
|
6022
|
+
throw new Error(
|
|
6023
|
+
`Cartridge registry from ${repoUrl}: declared fabricRegistryUrl '${data.fabricRegistryUrl}', ` +
|
|
6024
|
+
`and its charter names the fabric '${charter.fabric}'`
|
|
5675
6025
|
);
|
|
5676
6026
|
}
|
|
5677
6027
|
if (!data.channels || typeof data.channels !== 'object') {
|
|
@@ -6170,6 +6520,11 @@ const RELEASE_KEY_CERT_FORMAT = 'machinefabric-release-key-cert/1';
|
|
|
6170
6520
|
* Mirrors Rust `MANIFEST_SIG_FORMAT`. */
|
|
6171
6521
|
const MANIFEST_SIG_FORMAT = 'machinefabric-manifest-sig/1';
|
|
6172
6522
|
|
|
6523
|
+
/** Format discriminator for charter signature envelopes — its own word, so no
|
|
6524
|
+
* signed manifest can be passed off as a charter. Mirrors Rust
|
|
6525
|
+
* `CHARTER_SIG_FORMAT`. */
|
|
6526
|
+
const CHARTER_SIG_FORMAT = 'machinefabric-charter-sig/1';
|
|
6527
|
+
|
|
6173
6528
|
/**
|
|
6174
6529
|
* What a consumer concluded about a cartridge registry.
|
|
6175
6530
|
* Mirrors Rust `RegistryVerdictState` (bifaci/registry_verdict.rs).
|
|
@@ -6178,7 +6533,7 @@ const MANIFEST_SIG_FORMAT = 'machinefabric-manifest-sig/1';
|
|
|
6178
6533
|
* every cartridge that claims provenance from it. The vocabulary separates the
|
|
6179
6534
|
* two things a consumer can conclude — that it could not get an answer
|
|
6180
6535
|
* (OFFLINE, UNREACHABLE, HTTP_ERROR, MALFORMED) and that it got one and refused
|
|
6181
|
-
* it (UNSIGNED, UNTRUSTED, UNVERIFIABLE) — because those have opposite
|
|
6536
|
+
* it (UNCHARTERED, UNSIGNED, UNTRUSTED, UNVERIFIABLE) — because those have opposite
|
|
6182
6537
|
* remedies.
|
|
6183
6538
|
*/
|
|
6184
6539
|
const RegistryVerdictState = Object.freeze({
|
|
@@ -6197,6 +6552,10 @@ const RegistryVerdictState = Object.freeze({
|
|
|
6197
6552
|
HTTP_ERROR: 'http_error',
|
|
6198
6553
|
// The registry answered with a body this build cannot read as a manifest.
|
|
6199
6554
|
MALFORMED: 'malformed',
|
|
6555
|
+
// Nothing is at `<anchor>/charter.json` — the URL is not a registry
|
|
6556
|
+
// MachineFabric chartered, and nothing it serves is used. A charter that IS
|
|
6557
|
+
// there and is refused is UNTRUSTED or UNVERIFIABLE, with a charter_ reason.
|
|
6558
|
+
UNCHARTERED: 'unchartered',
|
|
6200
6559
|
// No signature sidecar where one is required.
|
|
6201
6560
|
UNSIGNED: 'unsigned',
|
|
6202
6561
|
// The chain was evaluated and REJECTED — the registry's problem.
|
|
@@ -6250,6 +6609,7 @@ const _REGISTRY_REMEDY_BY_STATE = Object.freeze({
|
|
|
6250
6609
|
unreachable: RegistryRemedy.CHECK_NETWORK,
|
|
6251
6610
|
http_error: RegistryRemedy.RETRY_LATER,
|
|
6252
6611
|
malformed: RegistryRemedy.RETRY_LATER,
|
|
6612
|
+
unchartered: RegistryRemedy.DO_NOT_PROCEED,
|
|
6253
6613
|
unsigned: RegistryRemedy.DO_NOT_PROCEED,
|
|
6254
6614
|
untrusted: RegistryRemedy.DO_NOT_PROCEED,
|
|
6255
6615
|
unverifiable: RegistryRemedy.UPDATE_CLIENT,
|
|
@@ -6279,6 +6639,30 @@ const ChainFailureReason = Object.freeze({
|
|
|
6279
6639
|
KEY_ID_MISMATCH: 'key_id_mismatch',
|
|
6280
6640
|
NO_AUTHORIZING_CERTIFICATE: 'no_authorizing_certificate',
|
|
6281
6641
|
MANIFEST_SIGNATURE_INVALID: 'manifest_signature_invalid',
|
|
6642
|
+
// The registry's charter is not a charter document, or breaks its shape.
|
|
6643
|
+
CHARTER_MALFORMED: 'charter_malformed',
|
|
6644
|
+
// The charter declares a format this build does not read.
|
|
6645
|
+
CHARTER_UNSUPPORTED_FORMAT: 'charter_unsupported_format',
|
|
6646
|
+
// The charter's signature does not verify over its bytes.
|
|
6647
|
+
CHARTER_SIGNATURE_INVALID: 'charter_signature_invalid',
|
|
6648
|
+
// The charter is for another anchor than the one asked.
|
|
6649
|
+
CHARTER_ANCHOR_MISMATCH: 'charter_anchor_mismatch',
|
|
6650
|
+
// The charter is for another kind of registry.
|
|
6651
|
+
CHARTER_KIND_MISMATCH: 'charter_kind_mismatch',
|
|
6652
|
+
// The charter's registry is signed under another environment than this build verifies.
|
|
6653
|
+
CHARTER_ENVIRONMENT_MISMATCH: 'charter_environment_mismatch',
|
|
6654
|
+
// The cartridge registry's charter names another fabric than this client's.
|
|
6655
|
+
CHARTER_FABRIC_MISMATCH: 'charter_fabric_mismatch',
|
|
6656
|
+
// The charter is issued in the future.
|
|
6657
|
+
CHARTER_NOT_YET_VALID: 'charter_not_yet_valid',
|
|
6658
|
+
// The charter's own validity has ended.
|
|
6659
|
+
CHARTER_EXPIRED: 'charter_expired',
|
|
6660
|
+
// A stored charter is in a folder not named for its slug.
|
|
6661
|
+
CHARTER_FOLDER_MISMATCH: 'charter_folder_mismatch',
|
|
6662
|
+
// The charter names a slug another registry's charter already holds.
|
|
6663
|
+
// Charters are unique by slug where they are issued; two with one slug is
|
|
6664
|
+
// an invalid state, and neither is given the other's folder.
|
|
6665
|
+
CHARTER_SLUG_TAKEN: 'charter_slug_taken',
|
|
6282
6666
|
});
|
|
6283
6667
|
|
|
6284
6668
|
const REGISTRY_VERDICT_STATES = Object.freeze(Object.values(RegistryVerdictState));
|
|
@@ -6292,6 +6676,8 @@ const UNEVALUABLE_CHAIN_FAILURES = Object.freeze([
|
|
|
6292
6676
|
ChainFailureReason.MALFORMED_CERTIFICATE,
|
|
6293
6677
|
ChainFailureReason.UNSUPPORTED_CERTIFICATE_FORMAT,
|
|
6294
6678
|
ChainFailureReason.EMPTY_CERTIFICATE_LIST,
|
|
6679
|
+
ChainFailureReason.CHARTER_MALFORMED,
|
|
6680
|
+
ChainFailureReason.CHARTER_UNSUPPORTED_FORMAT,
|
|
6295
6681
|
]);
|
|
6296
6682
|
|
|
6297
6683
|
/**
|
|
@@ -6325,7 +6711,8 @@ function registryVerdictIsTrustFailure(state) {
|
|
|
6325
6711
|
if (!REGISTRY_VERDICT_STATES.includes(state)) {
|
|
6326
6712
|
throw new Error(`unknown registry verdict state '${state}'`);
|
|
6327
6713
|
}
|
|
6328
|
-
return state === RegistryVerdictState.
|
|
6714
|
+
return state === RegistryVerdictState.UNCHARTERED
|
|
6715
|
+
|| state === RegistryVerdictState.UNSIGNED
|
|
6329
6716
|
|| state === RegistryVerdictState.UNTRUSTED
|
|
6330
6717
|
|| state === RegistryVerdictState.UNVERIFIABLE;
|
|
6331
6718
|
}
|
|
@@ -6376,7 +6763,7 @@ class RegistryVerdict {
|
|
|
6376
6763
|
return new RegistryVerdict(registryUrl, RegistryVerdictState.PENDING, '', null, null, 0);
|
|
6377
6764
|
}
|
|
6378
6765
|
|
|
6379
|
-
/** A state carrying only a detail line: OFFLINE, UNREACHABLE, MALFORMED, UNSIGNED. */
|
|
6766
|
+
/** A state carrying only a detail line: OFFLINE, UNREACHABLE, MALFORMED, UNCHARTERED, UNSIGNED. */
|
|
6380
6767
|
static stated(registryUrl, state, detail, checkedAtUnixSeconds) {
|
|
6381
6768
|
return new RegistryVerdict(registryUrl, state, detail, null, null, checkedAtUnixSeconds);
|
|
6382
6769
|
}
|
|
@@ -6497,7 +6884,7 @@ const CartridgeAttachmentErrorKind = Object.freeze({
|
|
|
6497
6884
|
ENTRY_POINT_MISSING: 'entry_point_missing',
|
|
6498
6885
|
QUARANTINED: 'quarantined',
|
|
6499
6886
|
// On-disk install context disagrees with the cartridge.json the cartridge
|
|
6500
|
-
// declares —
|
|
6887
|
+
// declares — its registry folder's charter names another registry, the
|
|
6501
6888
|
// channel folder doesn't match the manifest's channel, or a bundled-cartridge
|
|
6502
6889
|
// integrity proof failed. Structurally well-formed but cannot be trusted
|
|
6503
6890
|
// because its placement on disk does not match what it claims to be.
|
|
@@ -6703,9 +7090,8 @@ class InstalledCartridgeRecord {
|
|
|
6703
7090
|
// ============================================================================
|
|
6704
7091
|
//
|
|
6705
7092
|
// Mirrors capdag::cartridge_discovery plus its dependencies
|
|
6706
|
-
// capdag::bifaci::cartridge_json (CartridgeJson,
|
|
6707
|
-
//
|
|
6708
|
-
// capdag::bifaci::cartridge_slug::slug_for.
|
|
7093
|
+
// capdag::bifaci::cartridge_json (CartridgeJson, hash_cartridge_directory) and
|
|
7094
|
+
// the registry folders of capdag::bifaci::charter.
|
|
6709
7095
|
//
|
|
6710
7096
|
// This is the single source of truth for classifying each installed cartridge
|
|
6711
7097
|
// version directory as attachable (`directory`) or `incompatible`. Node
|
|
@@ -6713,20 +7099,6 @@ class InstalledCartridgeRecord {
|
|
|
6713
7099
|
// inside the functions so the browser bundle — which never performs disk
|
|
6714
7100
|
// discovery — never references them.
|
|
6715
7101
|
|
|
6716
|
-
/**
|
|
6717
|
-
* Synchronous on-disk slug for a registry URL. Mirrors
|
|
6718
|
-
* capdag::bifaci::cartridge_slug::slug_for byte-for-byte: `null`/`undefined`
|
|
6719
|
-
* (a dev cartridge) → the literal `DEV_SLUG`; otherwise a path-safe transform
|
|
6720
|
-
* of the URL's authority (host[:port]). Identical output to the async
|
|
6721
|
-
* `slugForRegistryUrl` above — no hashing, so both are trivially in sync.
|
|
6722
|
-
*
|
|
6723
|
-
* @param {string|null|undefined} registryUrl
|
|
6724
|
-
* @returns {string}
|
|
6725
|
-
*/
|
|
6726
|
-
function slugForSync(registryUrl) {
|
|
6727
|
-
return slugForRegistryUrlSync(registryUrl);
|
|
6728
|
-
}
|
|
6729
|
-
|
|
6730
7102
|
/**
|
|
6731
7103
|
* How a cartridge was installed. Exactly ONE value is semantic: BUNDLE
|
|
6732
7104
|
* selects the bundled-cartridge integrity path at discovery. Every other
|
|
@@ -6742,54 +7114,10 @@ const CartridgeInstallSource = Object.freeze({
|
|
|
6742
7114
|
APP_INSTALLER: 'app_installer',
|
|
6743
7115
|
});
|
|
6744
7116
|
|
|
6745
|
-
/**
|
|
6746
|
-
* Verdict from `validateRegistryUrlScheme`. Mirrors Rust
|
|
6747
|
-
* RegistryUrlSchemeResult: distinguishes OK from each rejection reason so
|
|
6748
|
-
* callers can render an actionable message. `kind` is one of `ok`,
|
|
6749
|
-
* `not_a_url`, `non_https`.
|
|
6750
|
-
*/
|
|
6751
|
-
const RegistryUrlSchemeResultKind = Object.freeze({
|
|
6752
|
-
OK: 'ok',
|
|
6753
|
-
NOT_A_URL: 'not_a_url',
|
|
6754
|
-
NON_HTTPS: 'non_https',
|
|
6755
|
-
});
|
|
6756
|
-
|
|
6757
|
-
/**
|
|
6758
|
-
* Validate that a non-null `registry_url` uses the `https` scheme — UNLESS
|
|
6759
|
-
* `devMode` is set, in which case any well-formed URL is accepted (so
|
|
6760
|
-
* developers can point at `http://localhost:port` during integration
|
|
6761
|
-
* testing). Mirrors capdag::bifaci::cartridge_json::validate_registry_url_scheme.
|
|
6762
|
-
*
|
|
6763
|
-
* Cheap parse: split once on `://`. The rule is "scheme must be the literal
|
|
6764
|
-
* bytes `https`" (case-insensitive); full URL validation is the caller's job.
|
|
6765
|
-
*
|
|
6766
|
-
* @param {string} url
|
|
6767
|
-
* @param {boolean} devMode
|
|
6768
|
-
* @returns {{kind: string, url?: string, scheme?: string}}
|
|
6769
|
-
*/
|
|
6770
|
-
function validateRegistryUrlScheme(url, devMode) {
|
|
6771
|
-
const idx = url.indexOf('://');
|
|
6772
|
-
if (idx < 0) {
|
|
6773
|
-
return { kind: RegistryUrlSchemeResultKind.NOT_A_URL, url };
|
|
6774
|
-
}
|
|
6775
|
-
const scheme = url.slice(0, idx);
|
|
6776
|
-
const rest = url.slice(idx + 3);
|
|
6777
|
-
if (rest.length === 0) {
|
|
6778
|
-
return { kind: RegistryUrlSchemeResultKind.NOT_A_URL, url };
|
|
6779
|
-
}
|
|
6780
|
-
if (devMode) {
|
|
6781
|
-
return { kind: RegistryUrlSchemeResultKind.OK };
|
|
6782
|
-
}
|
|
6783
|
-
if (scheme.toLowerCase() === 'https') {
|
|
6784
|
-
return { kind: RegistryUrlSchemeResultKind.OK };
|
|
6785
|
-
}
|
|
6786
|
-
return { kind: RegistryUrlSchemeResultKind.NON_HTTPS, scheme };
|
|
6787
|
-
}
|
|
6788
|
-
|
|
6789
7117
|
/**
|
|
6790
7118
|
* Error kinds for CartridgeJson reading/validation. Mirrors the variants of
|
|
6791
|
-
* Rust CartridgeJsonError. `
|
|
6792
|
-
*
|
|
7119
|
+
* Rust CartridgeJsonError. `REGISTRY_FOLDER_MISMATCH` is the charter-rule
|
|
7120
|
+
* failure that discovery maps to MISPLACED.
|
|
6793
7121
|
*/
|
|
6794
7122
|
const CartridgeJsonErrorKind = Object.freeze({
|
|
6795
7123
|
NOT_FOUND: 'not_found',
|
|
@@ -6798,12 +7126,12 @@ const CartridgeJsonErrorKind = Object.freeze({
|
|
|
6798
7126
|
ENTRY_POINT_MISSING: 'entry_point_missing',
|
|
6799
7127
|
ENTRY_POINT_NOT_EXECUTABLE: 'entry_point_not_executable',
|
|
6800
7128
|
ENTRY_PATH_ESCAPE: 'entry_path_escape',
|
|
6801
|
-
|
|
7129
|
+
REGISTRY_FOLDER_MISMATCH: 'registry_folder_mismatch',
|
|
6802
7130
|
});
|
|
6803
7131
|
|
|
6804
7132
|
/**
|
|
6805
7133
|
* Error thrown by CartridgeJson.readFromDir. Carries a structured `kind` so
|
|
6806
|
-
* discovery can map a
|
|
7134
|
+
* discovery can map a folder mismatch to MISPLACED and everything else
|
|
6807
7135
|
* to MANIFEST_INVALID. Mirrors Rust CartridgeJsonError.
|
|
6808
7136
|
*/
|
|
6809
7137
|
class CartridgeJsonError extends Error {
|
|
@@ -6897,19 +7225,20 @@ class CartridgeJson {
|
|
|
6897
7225
|
|
|
6898
7226
|
/**
|
|
6899
7227
|
* Read and validate a `cartridge.json` from a version directory, enforcing
|
|
6900
|
-
* the
|
|
6901
|
-
*
|
|
7228
|
+
* the charter rule against `folder` (the registry folder the host walked
|
|
7229
|
+
* through, already opened). Mirrors CartridgeJson::read_from_dir.
|
|
6902
7230
|
*
|
|
6903
|
-
* Validates: file exists and is valid JSON; the declared registry_url
|
|
6904
|
-
*
|
|
6905
|
-
* exists, does not escape the version directory, and is
|
|
7231
|
+
* Validates: file exists and is valid JSON; the declared registry_url is
|
|
7232
|
+
* the folder's anchor, or null in dev (else REGISTRY_FOLDER_MISMATCH); the
|
|
7233
|
+
* entry point exists, does not escape the version directory, and is
|
|
7234
|
+
* executable.
|
|
6906
7235
|
*
|
|
6907
7236
|
* @param {string} versionDir - Absolute path to the version directory.
|
|
6908
|
-
* @param {
|
|
7237
|
+
* @param {RegistryFolder} folder - The registry folder it sits in.
|
|
6909
7238
|
* @returns {CartridgeJson}
|
|
6910
7239
|
* @throws {CartridgeJsonError}
|
|
6911
7240
|
*/
|
|
6912
|
-
static readFromDir(versionDir,
|
|
7241
|
+
static readFromDir(versionDir, folder) {
|
|
6913
7242
|
const { fs, path } = nodeModules('reading cartridge.json');
|
|
6914
7243
|
const jsonPath = path.join(versionDir, 'cartridge.json');
|
|
6915
7244
|
|
|
@@ -6932,15 +7261,18 @@ class CartridgeJson {
|
|
|
6932
7261
|
}
|
|
6933
7262
|
const cj = CartridgeJson.fromObject(parsed);
|
|
6934
7263
|
|
|
6935
|
-
//
|
|
6936
|
-
//
|
|
6937
|
-
//
|
|
6938
|
-
|
|
6939
|
-
if (
|
|
7264
|
+
// The charter rule (places 1 + 2): the cartridge records exactly the
|
|
7265
|
+
// anchor its folder's charter states, or no registry in dev. Any other
|
|
7266
|
+
// pairing — null in a registry's folder, a URL in dev, or another
|
|
7267
|
+
// registry's anchor — is an installer bug or a tampered tree.
|
|
7268
|
+
if (cj.registryUrl !== folder.anchor) {
|
|
7269
|
+
const holds = folder.anchor === null
|
|
7270
|
+
? 'the cartridges built with no registry'
|
|
7271
|
+
: `the registry chartered for ${folder.anchor}`;
|
|
6940
7272
|
throw new CartridgeJsonError(
|
|
6941
|
-
CartridgeJsonErrorKind.
|
|
6942
|
-
`cartridge.json at ${jsonPath}:
|
|
6943
|
-
`
|
|
7273
|
+
CartridgeJsonErrorKind.REGISTRY_FOLDER_MISMATCH,
|
|
7274
|
+
`cartridge.json at ${jsonPath}: registry_url=${JSON.stringify(cj.registryUrl)}, ` +
|
|
7275
|
+
`but it sits in the folder '${folder.slug}', which holds ${holds}`
|
|
6944
7276
|
);
|
|
6945
7277
|
}
|
|
6946
7278
|
|
|
@@ -6981,11 +7313,6 @@ class CartridgeJson {
|
|
|
6981
7313
|
return this.registryUrl === null || this.registryUrl === undefined;
|
|
6982
7314
|
}
|
|
6983
7315
|
|
|
6984
|
-
/** The on-disk slug this provenance must live under. */
|
|
6985
|
-
registrySlug() {
|
|
6986
|
-
return slugForSync(this.registryUrl);
|
|
6987
|
-
}
|
|
6988
|
-
|
|
6989
7316
|
/**
|
|
6990
7317
|
* Resolve the absolute path to the entry point binary.
|
|
6991
7318
|
* @param {string} versionDir
|
|
@@ -7113,11 +7440,6 @@ class DiscoveryIdentity {
|
|
|
7113
7440
|
this.fabricManifestVersion = fabricManifestVersion;
|
|
7114
7441
|
this.cartridgeRegistryVersion = cartridgeRegistryVersion;
|
|
7115
7442
|
}
|
|
7116
|
-
|
|
7117
|
-
/** This host's own baked-registry slug (`dev` when registryUrl is null). */
|
|
7118
|
-
slug() {
|
|
7119
|
-
return slugForSync(this.registryUrl);
|
|
7120
|
-
}
|
|
7121
7443
|
}
|
|
7122
7444
|
|
|
7123
7445
|
/**
|
|
@@ -7438,10 +7760,11 @@ function probeCartridgeCapGroups(entryPath) {
|
|
|
7438
7760
|
|
|
7439
7761
|
/**
|
|
7440
7762
|
* Discover every cartridge under `{cartridgesRoot}/{slug}/{channel}/`. Scans
|
|
7441
|
-
* EVERY
|
|
7442
|
-
* `identity.registryUrl` does NOT restrict which
|
|
7443
|
-
* cartridge is validated in place against the
|
|
7444
|
-
*
|
|
7763
|
+
* EVERY registry folder present on disk (full parity) — the host's baked
|
|
7764
|
+
* `identity.registryUrl` does NOT restrict which folders are scanned; each
|
|
7765
|
+
* cartridge is validated in place against the registry folder it sits under,
|
|
7766
|
+
* as the charter stored there names it (the charter rule in
|
|
7767
|
+
* `CartridgeJson.readFromDir`). The channel folder IS
|
|
7445
7768
|
* pinned to the host's channel. An empty/absent scan root yields an empty
|
|
7446
7769
|
* roster; a real IO failure reading an existing root is an error.
|
|
7447
7770
|
*
|
|
@@ -7473,14 +7796,24 @@ async function discoverCartridges(cartridgesRoot, identity) {
|
|
|
7473
7796
|
// directories belong here. Skipped (logged in the reference).
|
|
7474
7797
|
continue;
|
|
7475
7798
|
}
|
|
7476
|
-
|
|
7799
|
+
// The folder is what its stored charter names, never what its own name
|
|
7800
|
+
// says. A folder whose charter is missing or refused still has its
|
|
7801
|
+
// cartridges surfaced — each as misplaced, with the reason — so the
|
|
7802
|
+
// operator sees what is there and why none of it runs.
|
|
7803
|
+
let folder = null;
|
|
7804
|
+
let folderError = null;
|
|
7805
|
+
try {
|
|
7806
|
+
folder = RegistryFolder.open(slugDir);
|
|
7807
|
+
} catch (e) {
|
|
7808
|
+
folderError = e;
|
|
7809
|
+
}
|
|
7477
7810
|
// {slug}/v{cartridgeRegistryVersion}/{channel}/… — the registry regime
|
|
7478
7811
|
// version is a path level pinned to the host's version (like the channel).
|
|
7479
7812
|
const scanRoot = path.join(slugDir, `v${identity.cartridgeRegistryVersion}`, identity.channel);
|
|
7480
7813
|
let scanStat;
|
|
7481
7814
|
try { scanStat = fs.statSync(scanRoot); } catch (_e) { continue; }
|
|
7482
7815
|
if (!scanStat.isDirectory()) continue;
|
|
7483
|
-
await scanChannelRoot(scanRoot,
|
|
7816
|
+
await scanChannelRoot(scanRoot, slugDir, folder, folderError, identity, discovered);
|
|
7484
7817
|
}
|
|
7485
7818
|
|
|
7486
7819
|
return discovered;
|
|
@@ -7488,10 +7821,11 @@ async function discoverCartridges(cartridgesRoot, identity) {
|
|
|
7488
7821
|
|
|
7489
7822
|
/**
|
|
7490
7823
|
* Scan one `{slug}/{channel}/` root: classify each cartridge name directory's
|
|
7491
|
-
* newest version against the host identity and the
|
|
7492
|
-
*
|
|
7824
|
+
* newest version against the host identity and the registry folder it sits
|
|
7825
|
+
* under — opened through the charter stored in it, or `folderError` why it
|
|
7826
|
+
* could not be. Mirrors capdag::cartridge_discovery::scan_channel_root.
|
|
7493
7827
|
*/
|
|
7494
|
-
async function scanChannelRoot(scanRoot,
|
|
7828
|
+
async function scanChannelRoot(scanRoot, slugDir, folder, folderError, identity, discovered) {
|
|
7495
7829
|
const { fs, path } = nodeModules('discovering cartridges');
|
|
7496
7830
|
|
|
7497
7831
|
let nameEntries;
|
|
@@ -7530,15 +7864,33 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
7530
7864
|
const pathDerivedVersion = path.basename(versionDir);
|
|
7531
7865
|
const detectedAt = unixSecondsNow();
|
|
7532
7866
|
|
|
7533
|
-
//
|
|
7534
|
-
//
|
|
7535
|
-
|
|
7536
|
-
|
|
7867
|
+
// A folder with no charter that reads makes every cartridge in it
|
|
7868
|
+
// misplaced, for the charter's reason.
|
|
7869
|
+
if (folder === null) {
|
|
7870
|
+
discovered.push(DiscoveredCartridge.incompatible({
|
|
7871
|
+
version_dir: versionDir,
|
|
7872
|
+
id: pathDerivedName,
|
|
7873
|
+
channel: identity.channel,
|
|
7874
|
+
registry_url: identity.registryUrl,
|
|
7875
|
+
version: pathDerivedVersion,
|
|
7876
|
+
error: new CartridgeAttachmentError(
|
|
7877
|
+
CartridgeAttachmentErrorKind.MISPLACED,
|
|
7878
|
+
`the registry folder ${slugDir} has no charter that reads: ${folderError.message}`,
|
|
7879
|
+
detectedAt
|
|
7880
|
+
),
|
|
7881
|
+
}));
|
|
7882
|
+
continue;
|
|
7883
|
+
}
|
|
7884
|
+
|
|
7885
|
+
// read_from_dir enforces the charter rule against the folder the cartridge
|
|
7886
|
+
// ACTUALLY sits in: its declared registry_url must be the folder's anchor.
|
|
7887
|
+
// A folder mismatch is misplaced; an unreadable/garbage cartridge.json is
|
|
7888
|
+
// ManifestInvalid. Both surfaced, never hosted.
|
|
7537
7889
|
let cj;
|
|
7538
7890
|
try {
|
|
7539
|
-
cj = CartridgeJson.readFromDir(versionDir,
|
|
7891
|
+
cj = CartridgeJson.readFromDir(versionDir, folder);
|
|
7540
7892
|
} catch (e) {
|
|
7541
|
-
const kind = (e instanceof CartridgeJsonError && e.kind === CartridgeJsonErrorKind.
|
|
7893
|
+
const kind = (e instanceof CartridgeJsonError && e.kind === CartridgeJsonErrorKind.REGISTRY_FOLDER_MISMATCH)
|
|
7542
7894
|
? CartridgeAttachmentErrorKind.MISPLACED
|
|
7543
7895
|
: CartridgeAttachmentErrorKind.MANIFEST_INVALID;
|
|
7544
7896
|
discovered.push(DiscoveredCartridge.incompatible({
|
|
@@ -7549,7 +7901,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
7549
7901
|
version: pathDerivedVersion,
|
|
7550
7902
|
error: new CartridgeAttachmentError(
|
|
7551
7903
|
kind,
|
|
7552
|
-
`cartridge.json failed to load
|
|
7904
|
+
`cartridge.json failed to load in the registry folder '${folder.slug}': ${e.message}`,
|
|
7553
7905
|
detectedAt
|
|
7554
7906
|
),
|
|
7555
7907
|
}));
|
|
@@ -7572,42 +7924,6 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
7572
7924
|
continue;
|
|
7573
7925
|
}
|
|
7574
7926
|
|
|
7575
|
-
// Scheme check is per-cartridge: a dev cartridge (null registry_url) never
|
|
7576
|
-
// reaches here; a registry cartridge must use https (devMode=false).
|
|
7577
|
-
if (cj.registryUrl !== null && cj.registryUrl !== undefined) {
|
|
7578
|
-
const res = validateRegistryUrlScheme(cj.registryUrl, false);
|
|
7579
|
-
if (res.kind === RegistryUrlSchemeResultKind.NON_HTTPS) {
|
|
7580
|
-
discovered.push(DiscoveredCartridge.incompatible({
|
|
7581
|
-
version_dir: versionDir,
|
|
7582
|
-
id: cj.name,
|
|
7583
|
-
channel: cj.channel,
|
|
7584
|
-
registry_url: cj.registryUrl,
|
|
7585
|
-
version: cj.version,
|
|
7586
|
-
error: new CartridgeAttachmentError(
|
|
7587
|
-
CartridgeAttachmentErrorKind.INCOMPATIBLE,
|
|
7588
|
-
`registry_url uses '${res.scheme}' scheme, must be https in non-dev builds. Rebuild the cartridge with an https registry URL.`,
|
|
7589
|
-
detectedAt
|
|
7590
|
-
),
|
|
7591
|
-
}));
|
|
7592
|
-
continue;
|
|
7593
|
-
}
|
|
7594
|
-
if (res.kind === RegistryUrlSchemeResultKind.NOT_A_URL) {
|
|
7595
|
-
discovered.push(DiscoveredCartridge.incompatible({
|
|
7596
|
-
version_dir: versionDir,
|
|
7597
|
-
id: cj.name,
|
|
7598
|
-
channel: cj.channel,
|
|
7599
|
-
registry_url: cj.registryUrl,
|
|
7600
|
-
version: cj.version,
|
|
7601
|
-
error: new CartridgeAttachmentError(
|
|
7602
|
-
CartridgeAttachmentErrorKind.INCOMPATIBLE,
|
|
7603
|
-
`registry_url '${res.url}' is not a well-formed URL.`,
|
|
7604
|
-
detectedAt
|
|
7605
|
-
),
|
|
7606
|
-
}));
|
|
7607
|
-
continue;
|
|
7608
|
-
}
|
|
7609
|
-
}
|
|
7610
|
-
|
|
7611
7927
|
if (cj.fabricManifestVersion !== identity.fabricManifestVersion) {
|
|
7612
7928
|
discovered.push(DiscoveredCartridge.incompatible({
|
|
7613
7929
|
version_dir: versionDir,
|
|
@@ -11141,6 +11457,8 @@ const FabricRegistryErrorCodes = Object.freeze({
|
|
|
11141
11457
|
PARSE: 'Parse',
|
|
11142
11458
|
/** What the registry served breaks a definition rule. */
|
|
11143
11459
|
VALIDATION: 'Validation',
|
|
11460
|
+
/** The registry's charter could not be had, or was refused: nothing it serves is used. */
|
|
11461
|
+
CHARTER: 'Charter',
|
|
11144
11462
|
});
|
|
11145
11463
|
|
|
11146
11464
|
/** Why the fabric registry did not give a definition. */
|
|
@@ -11283,7 +11601,8 @@ function checkedDim(obj, name, defver) {
|
|
|
11283
11601
|
*
|
|
11284
11602
|
* The manifest names every cap URN, media URN, alias and dim of its snapshot
|
|
11285
11603
|
* with the version of each definition; each definition is one object at
|
|
11286
|
-
* `<
|
|
11604
|
+
* the charter's `definition` location — `<anchor>/{caps,media,aliases,dims}/<sha256
|
|
11605
|
+
* of its canonical name>/<version>.json` in the publisher's layout.
|
|
11287
11606
|
* Nothing outside the pinned manifest is fetched: a URN it does not name is
|
|
11288
11607
|
* not found, never read from another snapshot.
|
|
11289
11608
|
*
|
|
@@ -11299,20 +11618,22 @@ function checkedDim(obj, name, defver) {
|
|
|
11299
11618
|
*/
|
|
11300
11619
|
class FabricRegistryClient {
|
|
11301
11620
|
/**
|
|
11302
|
-
* @param {string}
|
|
11621
|
+
* @param {string} anchor - The anchor the registry is chartered at; where
|
|
11622
|
+
* its manifests and definitions are is read from its charter
|
|
11303
11623
|
* @param {number} manifestVersion - The manifest this client reads
|
|
11304
11624
|
* @param {Object} [options]
|
|
11305
11625
|
* @param {function(string): Promise<Response>} [options.fetch] - How a URL
|
|
11306
11626
|
* is fetched (the runtime's `fetch` when not given)
|
|
11307
11627
|
*/
|
|
11308
|
-
constructor(
|
|
11309
|
-
if (typeof
|
|
11310
|
-
throw new Error('a fabric registry client is given the registry
|
|
11628
|
+
constructor(anchor, manifestVersion, options = {}) {
|
|
11629
|
+
if (typeof anchor !== 'string' || anchor.length === 0) {
|
|
11630
|
+
throw new Error('a fabric registry client is given the registry anchor');
|
|
11311
11631
|
}
|
|
11312
11632
|
if (!isPositiveWhole(manifestVersion)) {
|
|
11313
11633
|
throw new Error(`a fabric registry client reads a manifest version from 1 up, not ${JSON.stringify(manifestVersion)}`);
|
|
11314
11634
|
}
|
|
11315
|
-
this.
|
|
11635
|
+
this._anchor = anchor;
|
|
11636
|
+
this._charter = null;
|
|
11316
11637
|
this._manifestVersion = manifestVersion;
|
|
11317
11638
|
this._fetch = options.fetch || ((url) => globalThis.fetch(url));
|
|
11318
11639
|
this._manifest = null;
|
|
@@ -11327,20 +11648,47 @@ class FabricRegistryClient {
|
|
|
11327
11648
|
|
|
11328
11649
|
/**
|
|
11329
11650
|
* A client pinned to the manifest the registry has promoted as its latest
|
|
11330
|
-
* (
|
|
11651
|
+
* (the charter's `latest` location): for a host that follows the fabric
|
|
11331
11652
|
* rather than one snapshot of it.
|
|
11332
|
-
* @param {string}
|
|
11653
|
+
* @param {string} anchor
|
|
11333
11654
|
* @param {Object} [options] - As for the constructor
|
|
11334
11655
|
* @returns {Promise<FabricRegistryClient>}
|
|
11335
11656
|
*/
|
|
11336
|
-
static async latest(
|
|
11337
|
-
const probe = new FabricRegistryClient(
|
|
11338
|
-
const
|
|
11657
|
+
static async latest(anchor, options = {}) {
|
|
11658
|
+
const probe = new FabricRegistryClient(anchor, 1, options);
|
|
11659
|
+
const charter = await probe.charter();
|
|
11660
|
+
const url = charter.location('latest', {});
|
|
11339
11661
|
const latest = await probe._get(url, 'the latest manifest');
|
|
11340
11662
|
if (latest === null || typeof latest !== 'object' || !isPositiveWhole(latest.version)) {
|
|
11341
11663
|
throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE, `${url} does not name a manifest version`);
|
|
11342
11664
|
}
|
|
11343
|
-
|
|
11665
|
+
const client = new FabricRegistryClient(anchor, latest.version, options);
|
|
11666
|
+
client._charter = charter;
|
|
11667
|
+
return client;
|
|
11668
|
+
}
|
|
11669
|
+
|
|
11670
|
+
/**
|
|
11671
|
+
* The registry's charter, fetched once from `<anchor>/charter.json`.
|
|
11672
|
+
* Everything this client fetches is where the charter's locations say.
|
|
11673
|
+
* @returns {Promise<Charter>}
|
|
11674
|
+
*/
|
|
11675
|
+
async charter() {
|
|
11676
|
+
if (this._charter !== null) return this._charter;
|
|
11677
|
+
return this._once('charter', async () => {
|
|
11678
|
+
try {
|
|
11679
|
+
this._charter = await fetchCharter(this._anchor, RegistryKind.FABRIC, this._fetch);
|
|
11680
|
+
} catch (error) {
|
|
11681
|
+
throw new FabricRegistryError(FabricRegistryErrorCodes.CHARTER,
|
|
11682
|
+
`the fabric registry ${this._anchor} has no charter this client can use: ${error.message}`);
|
|
11683
|
+
}
|
|
11684
|
+
return this._charter;
|
|
11685
|
+
});
|
|
11686
|
+
}
|
|
11687
|
+
|
|
11688
|
+
/** The URL of one definition: `kind` is caps, media, aliases or dims; `name` what it hashes. */
|
|
11689
|
+
async _definitionUrl(kind, name, version) {
|
|
11690
|
+
const charter = await this.charter();
|
|
11691
|
+
return charter.location('definition', { kind, urn_hash: await sha256Hex(name), version });
|
|
11344
11692
|
}
|
|
11345
11693
|
|
|
11346
11694
|
/** The manifest version this client reads. */
|
|
@@ -11372,7 +11720,7 @@ class FabricRegistryClient {
|
|
|
11372
11720
|
async manifest() {
|
|
11373
11721
|
if (this._manifest !== null) return this._manifest;
|
|
11374
11722
|
return this._once('manifest', async () => {
|
|
11375
|
-
const url =
|
|
11723
|
+
const url = (await this.charter()).location('manifest', { manifest_version: this._manifestVersion });
|
|
11376
11724
|
this._manifest = manifestFromJSON(await this._get(url, `manifest v${this._manifestVersion}`), this._manifestVersion);
|
|
11377
11725
|
return this._manifest;
|
|
11378
11726
|
});
|
|
@@ -11405,7 +11753,7 @@ class FabricRegistryClient {
|
|
|
11405
11753
|
if (this._media.has(canonical)) return this._media.get(canonical);
|
|
11406
11754
|
return this._once(`media ${canonical}`, async () => {
|
|
11407
11755
|
const defver = await this._defver('media', canonical, `media definition '${canonical}'`);
|
|
11408
|
-
const url =
|
|
11756
|
+
const url = await this._definitionUrl('media', canonical, defver);
|
|
11409
11757
|
const def = mediaDefFromStored(await this._get(url, `media definition '${canonical}'`), canonical, defver);
|
|
11410
11758
|
await this._ensureDimsCached(`media def '${canonical}'`, def.dims);
|
|
11411
11759
|
this._media.set(canonical, def);
|
|
@@ -11427,7 +11775,7 @@ class FabricRegistryClient {
|
|
|
11427
11775
|
|
|
11428
11776
|
async _fetchCap(canonical) {
|
|
11429
11777
|
const defver = await this._defver('caps', canonical, `cap '${canonical}'`);
|
|
11430
|
-
const url =
|
|
11778
|
+
const url = await this._definitionUrl('caps', canonical, defver);
|
|
11431
11779
|
const cap = capFromStored(await this._get(url, `cap '${canonical}'`), canonical, defver);
|
|
11432
11780
|
// Every media URN the cap names, the bare `media:` aside (it is the
|
|
11433
11781
|
// universal type, not a definition).
|
|
@@ -11463,7 +11811,7 @@ class FabricRegistryClient {
|
|
|
11463
11811
|
|
|
11464
11812
|
async _fetchAlias(normalized) {
|
|
11465
11813
|
const defver = await this._defver('aliases', normalized, `alias '${normalized}'`);
|
|
11466
|
-
const url =
|
|
11814
|
+
const url = await this._definitionUrl('aliases', normalized, defver);
|
|
11467
11815
|
const obj = await this._get(url, `alias '${normalized}'`);
|
|
11468
11816
|
if (obj === null || typeof obj !== 'object' || typeof obj.name !== 'string' || typeof obj.target !== 'string') {
|
|
11469
11817
|
throw new FabricRegistryError(FabricRegistryErrorCodes.PARSE, `the registry's alias '${normalized}' is not an alias`);
|
|
@@ -11501,7 +11849,7 @@ class FabricRegistryClient {
|
|
|
11501
11849
|
}
|
|
11502
11850
|
|
|
11503
11851
|
async _fetchDim(name, defver) {
|
|
11504
|
-
const url =
|
|
11852
|
+
const url = await this._definitionUrl('dims', name, defver);
|
|
11505
11853
|
const dim = checkedDim(await this._get(url, `dim '${name}'`), name, defver);
|
|
11506
11854
|
this._dims.set(name, dim);
|
|
11507
11855
|
return dim;
|
|
@@ -12124,6 +12472,7 @@ export {
|
|
|
12124
12472
|
RegistryRemedy,
|
|
12125
12473
|
registryVerdictRemedy,
|
|
12126
12474
|
MANIFEST_SIG_FORMAT,
|
|
12475
|
+
CHARTER_SIG_FORMAT,
|
|
12127
12476
|
RELEASE_KEY_CERT_FORMAT,
|
|
12128
12477
|
ALIAS_TARGET_CAP,
|
|
12129
12478
|
ALIAS_TARGET_MEDIA,
|
|
@@ -12193,7 +12542,6 @@ export {
|
|
|
12193
12542
|
validateNoMediaDefRedefinition,
|
|
12194
12543
|
validateNoMediaDefRedefinitionSync,
|
|
12195
12544
|
validateNoMediaDefDuplicates,
|
|
12196
|
-
getSchemaBaseURL,
|
|
12197
12545
|
getProfileURL,
|
|
12198
12546
|
MEDIA_STRING,
|
|
12199
12547
|
MEDIA_INTEGER,
|
|
@@ -12336,10 +12684,7 @@ export {
|
|
|
12336
12684
|
// Build-env registry identity
|
|
12337
12685
|
registryUrlFromBuildEnv,
|
|
12338
12686
|
// Cartridge discovery
|
|
12339
|
-
slugForSync,
|
|
12340
12687
|
CartridgeInstallSource,
|
|
12341
|
-
RegistryUrlSchemeResultKind,
|
|
12342
|
-
validateRegistryUrlScheme,
|
|
12343
12688
|
CartridgeJsonErrorKind,
|
|
12344
12689
|
CartridgeJsonError,
|
|
12345
12690
|
CartridgeJson,
|
|
@@ -12350,11 +12695,21 @@ export {
|
|
|
12350
12695
|
DiscoveredCartridge,
|
|
12351
12696
|
probeCartridgeCapGroups,
|
|
12352
12697
|
discoverCartridges,
|
|
12353
|
-
// Registry
|
|
12698
|
+
// Registry charters
|
|
12699
|
+
CHARTER_FORMAT,
|
|
12700
|
+
CHARTER_FILE,
|
|
12701
|
+
CHARTER_SIG_FILE,
|
|
12354
12702
|
DEV_SLUG,
|
|
12355
|
-
|
|
12356
|
-
|
|
12703
|
+
RegistryKind,
|
|
12704
|
+
CharterErrorKind,
|
|
12705
|
+
CharterError,
|
|
12706
|
+
Charter,
|
|
12707
|
+
fetchCharter,
|
|
12708
|
+
readStoredCharter,
|
|
12709
|
+
storedCharterFor,
|
|
12710
|
+
RegistryFolder,
|
|
12357
12711
|
isRegistrySlug,
|
|
12712
|
+
isExperimentId,
|
|
12358
12713
|
// Machine notation
|
|
12359
12714
|
MachineSyntaxError,
|
|
12360
12715
|
MachineSyntaxErrorCodes,
|
package/formal/index.d.ts
CHANGED
package/formal/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Generated by lungo 1.
|
|
1
|
+
// Generated by lungo 1.116.0 from Lean 4.34.1 for program capdagFormal. Do not edit.
|
|
2
2
|
import * as L from "lungo-ts";
|
|
3
3
|
|
|
4
4
|
async function programModule() {
|
|
@@ -849,7 +849,7 @@ export const ASSURANCE = L.parseAssurance({
|
|
|
849
849
|
"provenance": {
|
|
850
850
|
"lean_version": "4.34.1",
|
|
851
851
|
"lean_githash": "5045d0056413266e57c625dcd7c365b10e377c52",
|
|
852
|
-
"lungo_version": "1.
|
|
852
|
+
"lungo_version": "1.116.0",
|
|
853
853
|
"bir_version": 3,
|
|
854
854
|
"runtime_abi": 2
|
|
855
855
|
},
|
package/formal/program.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Bahram Joharshamshiri",
|
|
3
3
|
"dependencies": {
|
|
4
|
-
"lungo-ts": "1.
|
|
4
|
+
"lungo-ts": "1.116.0",
|
|
5
5
|
"peggy": "^5.1.0",
|
|
6
|
-
"tagged-urn": "^1.
|
|
6
|
+
"tagged-urn": "^1.155.4"
|
|
7
7
|
},
|
|
8
8
|
"description": "JavaScript implementation of Cap URN (Capability Uniform Resource Names) with strict validation and matching",
|
|
9
9
|
"engines": {
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"test": "node capdag.test.js && node machine.test.js"
|
|
51
51
|
},
|
|
52
52
|
"type": "module",
|
|
53
|
-
"version": "1.
|
|
53
|
+
"version": "1.480.4"
|
|
54
54
|
}
|