capdag 1.473.7 → 1.478.15

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 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 `${getSchemaBaseURL()}/${profileName}`;
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
- // Cartridge registry slug
5191
+ // Registry charters
5213
5192
  // =============================================================================
5214
5193
  //
5215
- // Deterministic mapping from a registry URL to a top-level folder
5216
- // name under the cartridges install root. Mirrors
5217
- // capdag::cartridge_slug byte-for-byte: SHA-256 of the URL bytes,
5218
- // lowercase hex, first 16 chars. The literal string "dev" is
5219
- // reserved for dev cartridges that have no registry.
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
- // JS uses Web Crypto's SubtleCrypto for SHA-256. The function is
5222
- // async because `crypto.subtle.digest` returns a Promise; consumers
5223
- // in synchronous contexts must await.
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
- const DEV_SLUG = "dev";
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 authority (host[:port]) of a registry URL: after `://` up to the next
5229
- * `/`, `?`, or `#` (path/query/fragment discarded).
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 authorityOfRegistryUrl(url) {
5232
- const i = url.indexOf("://");
5233
- const afterScheme = i >= 0 ? url.slice(i + 3) : url;
5234
- const m = afterScheme.search(/[/?#]/);
5235
- return m >= 0 ? afterScheme.slice(0, m) : afterScheme;
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
- * Compute the on-disk slug for a registry URL. Mirrors
5240
- * capdag::cartridge_slug::slug_for byte-for-byte: `DEV_SLUG` for null/undefined,
5241
- * otherwise a path-safe transform of the URL's authority (host[:port]) —
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|null|undefined} registryUrl
5247
- * @returns {string}
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 slugForRegistryUrlSync(registryUrl) {
5250
- if (registryUrl === null || registryUrl === undefined) {
5251
- return DEV_SLUG;
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
- let out = "";
5254
- for (const ch of authorityOfRegistryUrl(registryUrl)) {
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
- return out;
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
- * Async wrapper retained for callers that `await` the slug. The computation is
5263
- * synchronous (no hashing) — the authority transform needs no crypto.
5264
- * @returns {Promise<string>}
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
- async function slugForRegistryUrl(registryUrl) {
5267
- return slugForRegistryUrlSync(registryUrl);
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
- function isRegistrySlug(s) {
5271
- return typeof s === "string"
5272
- && s.length > 0
5273
- && s !== DEV_SLUG
5274
- && /^[a-z0-9.-]+$/.test(s);
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/v7.json`): its cap groups carry each
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 = '7.0';
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`), served at
5602
- * `/v<version>/manifest`. v0 is the pre-versioning legacy at the bare
5603
- * `/manifest` path; a registry of any other version is refused, never
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 = 3;
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
- constructor(cacheTtlSeconds = 3600) {
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 registry (schema
5632
- * CARTRIDGE_REGISTRY_SCHEMA_VERSION) from a URL and flatten
5633
- * to a list of `CartridgeInfo`, one per `(channel, id)` pair.
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 response = await fetch(repoUrl);
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
- // A cartridge registry must declare the fabric its caps resolve against. The
5657
- // client cross-checks this equals its own fabric (so every registry it uses
5658
- // shares one fabric); here we require it is at least present.
5659
- if (typeof data.fabricRegistryUrl !== 'string' || data.fabricRegistryUrl.length === 0) {
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
- `does not match the URL it was fetched from. These must match byte-for-byte.`
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.UNSIGNED
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 — the slug folder doesn't match slug_for(registry_url), the
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, validate_registry_url_scheme,
6707
- // hash_cartridge_directory) and the synchronous slug helper from
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. `RegistrySlugMismatch` is the three-place
6792
- * consistency failure that discovery maps to MISPLACED.
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
- REGISTRY_SLUG_MISMATCH: 'registry_slug_mismatch',
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 slug mismatch to MISPLACED and everything else
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 three-place rule against `expectedSlug` (the slug folder the host
6901
- * walked through). Mirrors CartridgeJson::read_from_dir.
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
- * hashes to `expectedSlug` (else RegistrySlugMismatch); the entry point
6905
- * exists, does not escape the version directory, and is executable.
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 {string} expectedSlug - The on-disk slug folder name.
7237
+ * @param {RegistryFolder} folder - The registry folder it sits in.
6909
7238
  * @returns {CartridgeJson}
6910
7239
  * @throws {CartridgeJsonError}
6911
7240
  */
6912
- static readFromDir(versionDir, expectedSlug) {
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
- // Three-place consistency rule (places 1 + 2): the folder slug must match
6936
- // the slug derived from the provenance's registry_url. None+`dev` and
6937
- // Some(url)+slug(url) are the only valid pairings.
6938
- const derivedSlug = slugForSync(cj.registryUrl);
6939
- if (derivedSlug !== expectedSlug) {
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.REGISTRY_SLUG_MISMATCH,
6942
- `cartridge.json at ${jsonPath}: registry slug mismatch — registry_url=${JSON.stringify(cj.registryUrl)} ` +
6943
- `hashes to slug='${derivedSlug}' but the directory tree placed it under '${expectedSlug}'`
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 slug folder present on disk (full parity) — the host's baked
7442
- * `identity.registryUrl` does NOT restrict which slugs are scanned; each
7443
- * cartridge is validated in place against the slug folder it sits under (the
7444
- * three-place rule in `CartridgeJson.readFromDir`). The channel folder IS
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
- const expectedSlug = slugEntry.name;
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, expectedSlug, identity, discovered);
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 slug folder it sits under.
7492
- * Mirrors capdag::cartridge_discovery::scan_channel_root.
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, expectedSlug, identity, discovered) {
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
- // read_from_dir enforces the three-place rule against the ACTUAL slug
7534
- // folder (expectedSlug): the cartridge's declared registry_url must hash
7535
- // to it. A slug mismatch is a BadInstallation; an unreadable/garbage
7536
- // cartridge.json is ManifestInvalid. Both surfaced, never hosted.
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, expectedSlug);
7891
+ cj = CartridgeJson.readFromDir(versionDir, folder);
7540
7892
  } catch (e) {
7541
- const kind = (e instanceof CartridgeJsonError && e.kind === CartridgeJsonErrorKind.REGISTRY_SLUG_MISMATCH)
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 under slug '${expectedSlug}': ${e.message}`,
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
- * `<base>/{caps,media,aliases,dims}/<sha256 of its canonical name>/<version>.json`.
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} baseUrl - The registry's base URL
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(baseUrl, manifestVersion, options = {}) {
11309
- if (typeof baseUrl !== 'string' || baseUrl.length === 0) {
11310
- throw new Error('a fabric registry client is given the registry URL');
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._baseUrl = baseUrl.replace(/\/$/, '');
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
- * (`<base>/manifest/latest.json`): for a host that follows the fabric
11651
+ * (the charter's `latest` location): for a host that follows the fabric
11331
11652
  * rather than one snapshot of it.
11332
- * @param {string} baseUrl
11653
+ * @param {string} anchor
11333
11654
  * @param {Object} [options] - As for the constructor
11334
11655
  * @returns {Promise<FabricRegistryClient>}
11335
11656
  */
11336
- static async latest(baseUrl, options = {}) {
11337
- const probe = new FabricRegistryClient(baseUrl, 1, options);
11338
- const url = `${probe._baseUrl}/manifest/latest.json`;
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
- return new FabricRegistryClient(baseUrl, latest.version, options);
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 = `${this._baseUrl}/manifest/${this._manifestVersion}.json`;
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 = `${this._baseUrl}/media/${await sha256Hex(canonical)}/${defver}.json`;
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 = `${this._baseUrl}/caps/${await sha256Hex(canonical)}/${defver}.json`;
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 = `${this._baseUrl}/aliases/${await sha256Hex(normalized)}/${defver}.json`;
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 = `${this._baseUrl}/dims/${await sha256Hex(name)}/${defver}.json`;
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 slug
12698
+ // Registry charters
12699
+ CHARTER_FORMAT,
12700
+ CHARTER_FILE,
12701
+ CHARTER_SIG_FILE,
12354
12702
  DEV_SLUG,
12355
- slugForRegistryUrlSync,
12356
- slugForRegistryUrl,
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
@@ -1,4 +1,4 @@
1
- // Generated by lungo 1.115.34 from Lean 4.34.1 for program capdagFormal. Do not edit.
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
  /** Lean's CapDAG.Bifaci.Credit.Gate. */
package/formal/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // Generated by lungo 1.115.34 from Lean 4.34.1 for program capdagFormal. Do not edit.
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.115.34",
852
+ "lungo_version": "1.116.0",
853
853
  "bir_version": 3,
854
854
  "runtime_abi": 2
855
855
  },
Binary file
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "author": "Bahram Joharshamshiri",
3
3
  "dependencies": {
4
- "lungo-ts": "1.115.34",
4
+ "lungo-ts": "1.116.0",
5
5
  "peggy": "^5.1.0",
6
- "tagged-urn": "^1.151.0"
6
+ "tagged-urn": "^1.153.7"
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.473.7"
53
+ "version": "1.478.15"
54
54
  }