@xemahq/xema 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62888,6 +62888,39 @@ var AuthorizationLockCheck = Object.freeze({
62888
62888
  // edition/edition-config.mjs
62889
62889
  import { join as join3 } from "node:path";
62890
62890
 
62891
+ // edition/canonical-repository-url.mjs
62892
+ function canonicalPublicHttpsRepositoryUrl(raw, context) {
62893
+ if (typeof raw !== "string" || raw.length === 0 || raw.trim() !== raw) {
62894
+ throw new Error(`${context} must be a non-empty canonical HTTPS URL`);
62895
+ }
62896
+ let parsed;
62897
+ try {
62898
+ parsed = new URL(raw);
62899
+ } catch {
62900
+ throw new Error(`${context} is not a valid URL`);
62901
+ }
62902
+ if (parsed.protocol !== "https:") {
62903
+ throw new Error(`${context} must use HTTPS`);
62904
+ }
62905
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
62906
+ throw new Error(
62907
+ `${context} must not contain username/password credentials`
62908
+ );
62909
+ }
62910
+ if (parsed.search.length > 0 || parsed.hash.length > 0 || raw.includes("?") || raw.includes("#")) {
62911
+ throw new Error(`${context} must not contain a query or fragment`);
62912
+ }
62913
+ if (parsed.hostname.length === 0) {
62914
+ throw new Error(`${context} must identify a repository host`);
62915
+ }
62916
+ return parsed.toString();
62917
+ }
62918
+
62919
+ // edition/git-source-auth.mjs
62920
+ function isUnderSourcePrefix(canonicalUrl, prefix) {
62921
+ return canonicalUrl.startsWith(prefix) && canonicalUrl.length > prefix.length;
62922
+ }
62923
+
62891
62924
  // edition/strict-json.mjs
62892
62925
  import { readFileSync } from "node:fs";
62893
62926
  var DuplicateJsonKeyError = class extends Error {
@@ -63056,18 +63089,33 @@ function supplementalLauncher(value, where) {
63056
63089
  }
63057
63090
  return Object.freeze({ kind: value.kind, biomeId: text(value.biomeId, `${where}.biomeId`) });
63058
63091
  }
63092
+ var RetiredFirstPartySourceShapeError = class extends Error {
63093
+ constructor(repo, id) {
63094
+ super(
63095
+ `${FILE}: "firstPartySources"["${repo}"] is the retired source-id string ${JSON.stringify(id)}. Migrate it to { "sourceId": ${JSON.stringify(id)}, "repositoryUrl": "<the repository's canonical https URL>" }.`
63096
+ );
63097
+ this.name = "RetiredFirstPartySourceShapeError";
63098
+ }
63099
+ };
63059
63100
  function parseFirstPartySources(value) {
63060
63101
  const where = '"firstPartySources"';
63061
63102
  if (!isObject(value) || Object.keys(value).length === 0) {
63062
- fail(where, "must be a non-empty {repo handle: source id} object");
63103
+ fail(where, "must be a non-empty {repo handle: { sourceId, repositoryUrl }} object");
63063
63104
  }
63064
- const entries = Object.entries(value).map(([repo, id]) => {
63105
+ const entries = Object.entries(value).map(([repo, entry]) => {
63065
63106
  repoHandle(repo, `${where} key`);
63066
- if (typeof id !== "string" || !SOURCE_ID.test(id)) {
63067
- fail(`${where}["${repo}"]`, `must be a lowercase kebab-case source id; got ${JSON.stringify(id)}`);
63107
+ if (typeof entry === "string") {
63108
+ throw new RetiredFirstPartySourceShapeError(repo, entry);
63109
+ }
63110
+ const at = `${where}["${repo}"]`;
63111
+ exactObject(entry, at, ["sourceId", "repositoryUrl"]);
63112
+ if (typeof entry.sourceId !== "string" || !SOURCE_ID.test(entry.sourceId)) {
63113
+ fail(`${at}.sourceId`, `must be a lowercase kebab-case source id; got ${JSON.stringify(entry.sourceId)}`);
63068
63114
  }
63069
- return [repo, id];
63115
+ return [repo, Object.freeze({ sourceId: entry.sourceId, repositoryUrl: text(entry.repositoryUrl, `${at}.repositoryUrl`) })];
63070
63116
  });
63117
+ uniqueBy(entries, `${where} sourceId`, ([, entry]) => entry.sourceId);
63118
+ uniqueBy(entries, `${where} repositoryUrl`, ([, entry]) => entry.repositoryUrl);
63071
63119
  return Object.freeze(Object.fromEntries(entries));
63072
63120
  }
63073
63121
  function parseRuntimeBinding(value, where) {
@@ -63299,6 +63347,23 @@ function requireEvidenceTrustPolicy(config) {
63299
63347
  }
63300
63348
  return Object.freeze({ policy: value.policy, pin: value.pin });
63301
63349
  }
63350
+ function assertFirstPartyUrls(firstPartySources2, prefix) {
63351
+ for (const [repo, { repositoryUrl }] of Object.entries(firstPartySources2)) {
63352
+ const where = `${EDITION_WORKSPACE_CONFIG_FILE}: "firstPartySources"["${repo}"].repositoryUrl`;
63353
+ let canonical;
63354
+ try {
63355
+ canonical = canonicalPublicHttpsRepositoryUrl(repositoryUrl, where);
63356
+ } catch (err) {
63357
+ throw new EditionWorkspaceConfigError(err.message);
63358
+ }
63359
+ if (canonical !== repositoryUrl) {
63360
+ throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(repositoryUrl)} is not in its canonical form (${canonical})`);
63361
+ }
63362
+ if (!isUnderSourcePrefix(canonical, prefix)) {
63363
+ throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(repositoryUrl)} is not under sourceRepositoryUrlPrefix ${prefix}`);
63364
+ }
63365
+ }
63366
+ }
63302
63367
  function parseEditionWorkspaceConfig(document) {
63303
63368
  if (document === null || typeof document !== "object" || Array.isArray(document)) {
63304
63369
  throw new Error(`${EDITION_WORKSPACE_CONFIG_FILE} must be a JSON object`);
@@ -63314,11 +63379,15 @@ function parseEditionWorkspaceConfig(document) {
63314
63379
  const sections = Object.fromEntries(
63315
63380
  Object.entries(SECTIONS).filter(([key]) => document[key] !== void 0).map(([key, parse]) => [key, parse(document[key])])
63316
63381
  );
63382
+ const sourceRepositoryUrlPrefix = requireString(document, "sourceRepositoryUrlPrefix", SOURCE_URL_PREFIX, "https://<host>/<organisation>/ with a trailing slash");
63383
+ if (sections.firstPartySources !== void 0) {
63384
+ assertFirstPartyUrls(sections.firstPartySources, sourceRepositoryUrlPrefix);
63385
+ }
63317
63386
  return Object.freeze({
63318
63387
  ...sections,
63319
63388
  imageRegistry: requireString(document, "imageRegistry", IMAGE_REGISTRY, "<host>/<namespace> with no scheme or trailing slash"),
63320
63389
  ...optionalChartSet(document),
63321
- sourceRepositoryUrlPrefix: requireString(document, "sourceRepositoryUrlPrefix", SOURCE_URL_PREFIX, "https://<host>/<organisation>/ with a trailing slash"),
63390
+ sourceRepositoryUrlPrefix,
63322
63391
  sourceAuthUsername: requireSourceAuthUsername(document),
63323
63392
  evidenceTrustPolicy: requireEvidenceTrustPolicy(document)
63324
63393
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "artifactSha256": "sha256:7fa4aabdd08ee4c39e6f08a9e28a081ec1d3080fbd043e40ade1f0ecdfac65fe",
3
+ "artifactSha256": "sha256:945753c501638be9393cd1ebc697d58c2ddb11adbdebcdac7ae045e2cd34bf9b",
4
4
  "esbuild": {
5
5
  "version": "0.28.2"
6
6
  },
@@ -2419,253 +2419,277 @@
2419
2419
  },
2420
2420
  {
2421
2421
  "package": "@xemahq/xema",
2422
- "version": "0.43.0",
2422
+ "version": "0.44.0",
2423
2423
  "path": "edition/adapter-host-versions.mjs",
2424
2424
  "sha256": "sha256:1b7ebd3592f4da1225fba74aaba912e7d85fbceeb1626f7dcdac83491cbb4d69"
2425
2425
  },
2426
2426
  {
2427
2427
  "package": "@xemahq/xema",
2428
- "version": "0.43.0",
2428
+ "version": "0.44.0",
2429
2429
  "path": "edition/authorization-base.mjs",
2430
2430
  "sha256": "sha256:bbeac7ff01376cb89ed2140526d4795d8e559b3f01612b404a701f19aaa24c6a"
2431
2431
  },
2432
2432
  {
2433
2433
  "package": "@xemahq/xema",
2434
- "version": "0.43.0",
2434
+ "version": "0.44.0",
2435
2435
  "path": "edition/build-ledger.mjs",
2436
2436
  "sha256": "sha256:05202cbdd606aee086c8e9c62d28be68c429653a8b1d73a289ed0fe4ff14da85"
2437
2437
  },
2438
2438
  {
2439
2439
  "package": "@xemahq/xema",
2440
- "version": "0.43.0",
2440
+ "version": "0.44.0",
2441
+ "path": "edition/canonical-repository-url.mjs",
2442
+ "sha256": "sha256:91e7590afba782a3ee2fa64084e7c715e6a5b52c11e8a9cc68c64397f181b888"
2443
+ },
2444
+ {
2445
+ "package": "@xemahq/xema",
2446
+ "version": "0.44.0",
2441
2447
  "path": "edition/connector-source-authority.mjs",
2442
2448
  "sha256": "sha256:ea94475f4324737673accd7de1303c44db1cbc4dac61a865a06cd83abe310164"
2443
2449
  },
2444
2450
  {
2445
2451
  "package": "@xemahq/xema",
2446
- "version": "0.43.0",
2452
+ "version": "0.44.0",
2453
+ "path": "edition/develop-graph.mjs",
2454
+ "sha256": "sha256:319db77e708dd73a9dd4a81a963d86e52b030cad3346b104c0862af4e38a2237"
2455
+ },
2456
+ {
2457
+ "package": "@xemahq/xema",
2458
+ "version": "0.44.0",
2447
2459
  "path": "edition/dispatched-runtime-components.mjs",
2448
2460
  "sha256": "sha256:9712823d183a60108cd002b2d36a9e5c0867b9d576dd02825264a18f7b8ece69"
2449
2461
  },
2450
2462
  {
2451
2463
  "package": "@xemahq/xema",
2452
- "version": "0.43.0",
2464
+ "version": "0.44.0",
2453
2465
  "path": "edition/distribution-paths.mjs",
2454
2466
  "sha256": "sha256:d78a5c8405bc6c25d36fcec61b0030e1fdacac7d528e698adb96f906f4b70279"
2455
2467
  },
2456
2468
  {
2457
2469
  "package": "@xemahq/xema",
2458
- "version": "0.43.0",
2470
+ "version": "0.44.0",
2459
2471
  "path": "edition/edition-artifact.mjs",
2460
2472
  "sha256": "sha256:282b6d5e70736f7e41cff41167d2cd99b7d0127dcc2b712132888dcfc82e36cd"
2461
2473
  },
2462
2474
  {
2463
2475
  "package": "@xemahq/xema",
2464
- "version": "0.43.0",
2476
+ "version": "0.44.0",
2465
2477
  "path": "edition/edition-channel.mjs",
2466
2478
  "sha256": "sha256:3c0c1564dc50be133cad500452d953353aa5beb703142c05a17bbce8175ba9f3"
2467
2479
  },
2468
2480
  {
2469
2481
  "package": "@xemahq/xema",
2470
- "version": "0.43.0",
2482
+ "version": "0.44.0",
2471
2483
  "path": "edition/edition-chart-set.mjs",
2472
2484
  "sha256": "sha256:f9f16b58279c8c2619a39c5cc92bdbc334543d1a324304cd5d3b120d66dc8f3a"
2473
2485
  },
2474
2486
  {
2475
2487
  "package": "@xemahq/xema",
2476
- "version": "0.43.0",
2488
+ "version": "0.44.0",
2477
2489
  "path": "edition/edition-config.mjs",
2478
- "sha256": "sha256:e3997bd9a0082e02d01760868b2a4f41ed3081e904126b30fdf86e7938dc6137"
2490
+ "sha256": "sha256:7604e79a480c9b9db1d1c256155f05298adf6caa49da95ad232f6be581f3c72e"
2479
2491
  },
2480
2492
  {
2481
2493
  "package": "@xemahq/xema",
2482
- "version": "0.43.0",
2494
+ "version": "0.44.0",
2483
2495
  "path": "edition/edition-constants.mjs",
2484
2496
  "sha256": "sha256:d3747ca300f38832796521b47d12076bdba7f214199a1dec52c9384ab3dde394"
2485
2497
  },
2486
2498
  {
2487
2499
  "package": "@xemahq/xema",
2488
- "version": "0.43.0",
2500
+ "version": "0.44.0",
2489
2501
  "path": "edition/edition-contract-parse.mjs",
2490
2502
  "sha256": "sha256:c575e96b70bda82899931eb0b18567988e2e13e664fa498bcbef96b571dcabb4"
2491
2503
  },
2492
2504
  {
2493
2505
  "package": "@xemahq/xema",
2494
- "version": "0.43.0",
2506
+ "version": "0.44.0",
2495
2507
  "path": "edition/edition-contract.json",
2496
2508
  "sha256": "sha256:af7c88854007e63e4e77863cf72610f59ff838959dbd381087ec50ba782ae0e1"
2497
2509
  },
2498
2510
  {
2499
2511
  "package": "@xemahq/xema",
2500
- "version": "0.43.0",
2512
+ "version": "0.44.0",
2501
2513
  "path": "edition/edition-protocol.mjs",
2502
2514
  "sha256": "sha256:1f10ece5f34b6a6b0dbe169b6cbabe4b1b0778a1bc136c9c5c29b54181b1f546"
2503
2515
  },
2504
2516
  {
2505
2517
  "package": "@xemahq/xema",
2506
- "version": "0.43.0",
2518
+ "version": "0.44.0",
2507
2519
  "path": "edition/edition-source-pins.mjs",
2508
2520
  "sha256": "sha256:b141474fff33b06734fb62cd2552752b991c8267bb1c9bccb22a81dd80e91c6c"
2509
2521
  },
2510
2522
  {
2511
2523
  "package": "@xemahq/xema",
2512
- "version": "0.43.0",
2524
+ "version": "0.44.0",
2513
2525
  "path": "edition/edition-workspace.mjs",
2514
2526
  "sha256": "sha256:892b360f9f80d2308b0b12f9b99658ae06173878d9b71cf5d6fc8bd717b3c86e"
2515
2527
  },
2516
2528
  {
2517
2529
  "package": "@xemahq/xema",
2518
- "version": "0.43.0",
2530
+ "version": "0.44.0",
2519
2531
  "path": "edition/evidence-checkout.mjs",
2520
2532
  "sha256": "sha256:31fb669769b31de4c6f2b2501bb96959548172ef166e7568edbbfed7f5079a11"
2521
2533
  },
2522
2534
  {
2523
2535
  "package": "@xemahq/xema",
2524
- "version": "0.43.0",
2536
+ "version": "0.44.0",
2525
2537
  "path": "edition/evidence-grammars.mjs",
2526
2538
  "sha256": "sha256:4dae22b61ade99219a482b971c9780d68a3d37d021e39cdfe860b245364b8b9f"
2527
2539
  },
2528
2540
  {
2529
2541
  "package": "@xemahq/xema",
2530
- "version": "0.43.0",
2542
+ "version": "0.44.0",
2531
2543
  "path": "edition/evidence-trust-pin.mjs",
2532
2544
  "sha256": "sha256:65077589d1d9bd129c175c03a6dca6bb4eaf277fa29d6391e1065654fe8cc246"
2533
2545
  },
2534
2546
  {
2535
2547
  "package": "@xemahq/xema",
2536
- "version": "0.43.0",
2548
+ "version": "0.44.0",
2549
+ "path": "edition/first-party-source-ids.mjs",
2550
+ "sha256": "sha256:20cb1ad7cfb8c6e6686a6be5a51289da4bba76c20b58d9c1b5b6b3813a38dc1c"
2551
+ },
2552
+ {
2553
+ "package": "@xemahq/xema",
2554
+ "version": "0.44.0",
2537
2555
  "path": "edition/git-json.mjs",
2538
2556
  "sha256": "sha256:d0d25cda0c19007becabc9a2827c3dcf759b295cd0b41596eb4480c9549a1f21"
2539
2557
  },
2540
2558
  {
2541
2559
  "package": "@xemahq/xema",
2542
- "version": "0.43.0",
2560
+ "version": "0.44.0",
2561
+ "path": "edition/git-source-auth.mjs",
2562
+ "sha256": "sha256:c1b32aa6f7d59798db58238d632cafeadbd4f07376396a31acdeb5f0ac56dcac"
2563
+ },
2564
+ {
2565
+ "package": "@xemahq/xema",
2566
+ "version": "0.44.0",
2543
2567
  "path": "edition/image-delivery-entitlement-descriptor.mjs",
2544
2568
  "sha256": "sha256:4726b66116f01bf2523fafe2b7a356591eca8fb5ca6bd8b154dd4821873e921b"
2545
2569
  },
2546
2570
  {
2547
2571
  "package": "@xemahq/xema",
2548
- "version": "0.43.0",
2572
+ "version": "0.44.0",
2549
2573
  "path": "edition/image-evidence-replay-state.mjs",
2550
2574
  "sha256": "sha256:eba0cf1d38af0ab2f00c5099fa8bc2edc543136ce92f895544d4d284b3d6af0f"
2551
2575
  },
2552
2576
  {
2553
2577
  "package": "@xemahq/xema",
2554
- "version": "0.43.0",
2578
+ "version": "0.44.0",
2555
2579
  "path": "edition/image-evidence.mjs",
2556
2580
  "sha256": "sha256:8ccbd4e7ce482cbb25521e893ac047d31ca1da782e87ee2179629e8911aa6aa3"
2557
2581
  },
2558
2582
  {
2559
2583
  "package": "@xemahq/xema",
2560
- "version": "0.43.0",
2584
+ "version": "0.44.0",
2561
2585
  "path": "edition/kernel-capability-domain.mjs",
2562
2586
  "sha256": "sha256:3ecf887dd8d399e91a8285f6f115f6e418fb59bbf3f004539fe65ec667219356"
2563
2587
  },
2564
2588
  {
2565
2589
  "package": "@xemahq/xema",
2566
- "version": "0.43.0",
2590
+ "version": "0.44.0",
2567
2591
  "path": "edition/kernel-contract-authority.mjs",
2568
2592
  "sha256": "sha256:20abdde62a7b037c01ea9bbe1ff1e6094e6b0efd0add2f656f1938a04409bf82"
2569
2593
  },
2570
2594
  {
2571
2595
  "package": "@xemahq/xema",
2572
- "version": "0.43.0",
2596
+ "version": "0.44.0",
2573
2597
  "path": "edition/lib.mjs",
2574
- "sha256": "sha256:7ed1bf641f501ec6a85d4fe24c4373a2e3885b799489dd33c77ff874fc138b25"
2598
+ "sha256": "sha256:2cf8f4b52f9532f536a27c8bf6736cc760d7026754945209b1cf5be23391ef49"
2575
2599
  },
2576
2600
  {
2577
2601
  "package": "@xemahq/xema",
2578
- "version": "0.43.0",
2602
+ "version": "0.44.0",
2579
2603
  "path": "edition/no-follow-file.mjs",
2580
2604
  "sha256": "sha256:c17bdf5e441f3c079aa06d4b48bbed05cad087855b9ad2a5e6dc43d5ee2b17c3"
2581
2605
  },
2582
2606
  {
2583
2607
  "package": "@xemahq/xema",
2584
- "version": "0.43.0",
2608
+ "version": "0.44.0",
2585
2609
  "path": "edition/operation-requirements.mjs",
2586
2610
  "sha256": "sha256:92656c5e920fca2a5a75c0b8db133d551ba044c84a149f33435479248b3dff8b"
2587
2611
  },
2588
2612
  {
2589
2613
  "package": "@xemahq/xema",
2590
- "version": "0.43.0",
2614
+ "version": "0.44.0",
2591
2615
  "path": "edition/pin-source.mjs",
2592
- "sha256": "sha256:a23fef4698ef66ba9de4a1aabe3eebe6487d1f105f3fc161bafc5a33b99a2440"
2616
+ "sha256": "sha256:32fa918729a83587b4d3bca6e5fe381f5a525397ebc49d52729ec1f8cf216c37"
2593
2617
  },
2594
2618
  {
2595
2619
  "package": "@xemahq/xema",
2596
- "version": "0.43.0",
2620
+ "version": "0.44.0",
2597
2621
  "path": "edition/sigstore-bundle-media-type.mjs",
2598
2622
  "sha256": "sha256:e0aa5a3992f0385ddbbe69eb0307d642ac428ef516c8394816ac4fd52e0ac39c"
2599
2623
  },
2600
2624
  {
2601
2625
  "package": "@xemahq/xema",
2602
- "version": "0.43.0",
2626
+ "version": "0.44.0",
2603
2627
  "path": "edition/source-authority.mjs",
2604
2628
  "sha256": "sha256:9c351f60e8ea838f445f89c3709c60b7f13b021b78d6e40eaef91350a793f0ff"
2605
2629
  },
2606
2630
  {
2607
2631
  "package": "@xemahq/xema",
2608
- "version": "0.43.0",
2632
+ "version": "0.44.0",
2609
2633
  "path": "edition/strict-json.mjs",
2610
2634
  "sha256": "sha256:f95778396df7f38b086f55d2ec4f3b6644a8aeff2b32282a62829145fb544562"
2611
2635
  },
2612
2636
  {
2613
2637
  "package": "@xemahq/xema",
2614
- "version": "0.43.0",
2638
+ "version": "0.44.0",
2615
2639
  "path": "edition/string-order.mjs",
2616
2640
  "sha256": "sha256:b3210cd273e4b2e01fb8b1a91e28734cd211d4010c080d0ce95f5e5ff631cffb"
2617
2641
  },
2618
2642
  {
2619
2643
  "package": "@xemahq/xema",
2620
- "version": "0.43.0",
2644
+ "version": "0.44.0",
2621
2645
  "path": "edition/trust-policy-arming.mjs",
2622
2646
  "sha256": "sha256:6f6f192118407e71a744087caea988bcf3d33aca9803217bceb8134d7e2b9120"
2623
2647
  },
2624
2648
  {
2625
2649
  "package": "@xemahq/xema",
2626
- "version": "0.43.0",
2650
+ "version": "0.44.0",
2627
2651
  "path": "edition/trusted-executable.mjs",
2628
2652
  "sha256": "sha256:37d46cdbc999ddd55d4ea8aeed4850ddd1efde89ed7b532f5a2ddec87105d796"
2629
2653
  },
2630
2654
  {
2631
2655
  "package": "@xemahq/xema",
2632
- "version": "0.43.0",
2656
+ "version": "0.44.0",
2633
2657
  "path": "edition/verifier-binding.mjs",
2634
2658
  "sha256": "sha256:80732658617f5255258c538eeb2383331ec8553b44b58d552b567845a7b916aa"
2635
2659
  },
2636
2660
  {
2637
2661
  "package": "@xemahq/xema",
2638
- "version": "0.43.0",
2662
+ "version": "0.44.0",
2639
2663
  "path": "edition/verifier-files.mjs",
2640
2664
  "sha256": "sha256:cdd14a20a63f0f194a8abb55680a5a7e3ced1f09f8b210a90d349e655f6de0c0"
2641
2665
  },
2642
2666
  {
2643
2667
  "package": "@xemahq/xema",
2644
- "version": "0.43.0",
2668
+ "version": "0.44.0",
2645
2669
  "path": "edition/verifier-provenance.mjs",
2646
2670
  "sha256": "sha256:3e2fb8e07cf1f66b8ad6c7ac2555fe3af64ec7abeb07cecbaf1939a18c2b3663"
2647
2671
  },
2648
2672
  {
2649
2673
  "package": "@xemahq/xema",
2650
- "version": "0.43.0",
2674
+ "version": "0.44.0",
2651
2675
  "path": "edition/verify-catalogue-evidence.mjs",
2652
2676
  "sha256": "sha256:d7e42efc9edced97e4330cddc46183445a870ba22f57896a7ea6bc600d369137"
2653
2677
  },
2654
2678
  {
2655
2679
  "package": "@xemahq/xema",
2656
- "version": "0.43.0",
2680
+ "version": "0.44.0",
2657
2681
  "path": "edition/verify-image-evidence.mjs",
2658
2682
  "sha256": "sha256:396936f3d9e0ed68902b8b03c6fa02479e439357e6194ac8ce0e0991be834706"
2659
2683
  },
2660
2684
  {
2661
2685
  "package": "@xemahq/xema",
2662
- "version": "0.43.0",
2686
+ "version": "0.44.0",
2663
2687
  "path": "edition/workspace-sections.mjs",
2664
- "sha256": "sha256:383f37ce066ea5573c30cedf124022095bf94d050f0a266a5881f587cff0ced0"
2688
+ "sha256": "sha256:18c28e9fa2a6b7d8589c8434fc6ed9f1e93954d41339813e711ed0fbbcf5c61d"
2665
2689
  },
2666
2690
  {
2667
2691
  "package": "@xemahq/xema",
2668
- "version": "0.43.0",
2692
+ "version": "0.44.0",
2669
2693
  "path": "scripts/build-evidence-verifier.mjs",
2670
2694
  "sha256": "sha256:5a688d20c47dc88aff37aa0c6cc02daf2c370a5bc610fef8446ce7be222d4c0b"
2671
2695
  },
package/edition/README.md CHANGED
@@ -106,6 +106,20 @@ It refuses on:
106
106
 
107
107
  It reads no lock content (the committed `distribution`, `images`, `authorization` and per-edition `chart-set` locks; the trust-arming rule counts lock files by presence) and no source checkout at a pinned commit. The sources root is read only for the kernel contract checkout the runtime-image-source index check verifies its authority against. `--check-sources` is refused: it is the release check's source audit.
108
108
 
109
+ ## Develop graph
110
+
111
+ The develop graph is the data root's first-party repositories, declared in `edition-workspace.json`:
112
+
113
+ ```json
114
+ "firstPartySources": {
115
+ "repos/xema-base": { "sourceId": "xema-base", "repositoryUrl": "https://git.example.com/acme/xema-base.git" }
116
+ }
117
+ ```
118
+
119
+ - Each key is a repository handle (`repos/<x>` or `submodules/<x>`); `sourceId` is its kebab-case source id; `repositoryUrl` is canonical HTTPS under `sourceRepositoryUrlPrefix`. Both are unique. The retired string form (`"repos/x": "x"`) is refused with `RetiredFirstPartySourceShapeError`.
120
+ - `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
121
+ - A first-party repository is in the develop graph before any release pins it. The converse is checked offline: every `repositoryPins` entry must be declared first-party at the same `repositoryUrl`.
122
+
109
123
  ## Develop plane invariants
110
124
 
111
125
  `xema edition develop resolve` asserts the capability-domain reservation table and biome confinement on the candidate it resolves, against the develop graph it froze, before any later phase. On develop they are asserted only there; `xema edition check` asserts them over the committed locks.
@@ -3,9 +3,9 @@
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
5
5
 
6
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
6
7
  import { platformServiceIndexPath , WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
7
8
  import {
8
- canonicalPublicHttpsRepositoryUrl,
9
9
  loadKernelDistributionSchemas,
10
10
  readGitJsonAtCommit,
11
11
  serializeJson,
@@ -5,10 +5,12 @@ import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
5
5
  import { isAbsolute, join } from 'node:path';
6
6
 
7
7
  import { buildValidatedPlatformServiceIndex } from './build-platform-service-index.mjs';
8
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
8
9
  import { connectorCrossRepoBuildInputs } from './connector-source-authority.mjs';
9
10
  import { editionWorkspaceConfig, workspaceSection } from './edition-config.mjs';
10
11
  import { biomeIndexPath, dataRoot, platformServiceIndexPath, runtimeImageSourceIndexPath, WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath } from './edition-workspace.mjs';
11
12
  import { COMMIT_PATTERN } from './evidence-grammars.mjs';
13
+ import { firstPartySources } from './first-party-source-ids.mjs';
12
14
  import {
13
15
  buildTrustedKernelContractAuthority,
14
16
  KERNEL_COMMIT_FLAG,
@@ -17,7 +19,6 @@ import {
17
19
  } from './kernel-contract-authority.mjs';
18
20
  import {
19
21
  buildBiomeIndex,
20
- canonicalPublicHttpsRepositoryUrl,
21
22
  compareStrings,
22
23
  distributionBiomeSourceRepositories,
23
24
  loadKernelDistributionSchemas,
@@ -572,6 +573,28 @@ function compareSubject(left, right) {
572
573
  );
573
574
  }
574
575
 
576
+ /**
577
+ * Every release pin names a first-party repository (`firstPartySources`) at
578
+ * the URL declared there. The converse is not required: a first-party
579
+ * repository is in the develop graph before any release pins it.
580
+ */
581
+ function assertPinsAreFirstParty(repositoryPins) {
582
+ const declared = firstPartySources('checking that every release pin is a first-party repository');
583
+ for (const pin of repositoryPins) {
584
+ const source = declared[pin.repo];
585
+ if (source === undefined) {
586
+ throw new Error(
587
+ `runtime-image-source-index pins ${JSON.stringify(pin.repo)}, which edition-workspace.json does not declare under "firstPartySources"`,
588
+ );
589
+ }
590
+ if (source.repositoryUrl !== pin.repositoryUrl) {
591
+ throw new Error(
592
+ `runtime-image-source-index pins ${pin.repo} at ${pin.repositoryUrl}, but "firstPartySources" declares it at ${source.repositoryUrl}`,
593
+ );
594
+ }
595
+ }
596
+ }
597
+
575
598
  export async function validateRuntimeImageSourceIndex(
576
599
  index,
577
600
  schemas,
@@ -658,6 +681,7 @@ export async function validateRuntimeImageSourceIndex(
658
681
  throw new Error(`${repository.repo} repositoryUrl is not canonical`);
659
682
  }
660
683
  }
684
+ assertPinsAreFirstParty(index.repositoryPins);
661
685
  // Every subject a biome contributes to the image plane — BOTH the standing
662
686
  // services and the runtime components. A runtime component is a full image
663
687
  // subject (built, signed, mirrored) that simply is never scheduled, so a
@@ -0,0 +1,35 @@
1
+ // The canonical form of a public HTTPS repository URL, and the refusals that
2
+ // define it. Leaf module: the workspace config and the git source hold read it
3
+ // without importing the edition library.
4
+
5
+ export function canonicalPublicHttpsRepositoryUrl(raw, context) {
6
+ if (typeof raw !== 'string' || raw.length === 0 || raw.trim() !== raw) {
7
+ throw new Error(`${context} must be a non-empty canonical HTTPS URL`);
8
+ }
9
+ let parsed;
10
+ try {
11
+ parsed = new URL(raw);
12
+ } catch {
13
+ throw new Error(`${context} is not a valid URL`);
14
+ }
15
+ if (parsed.protocol !== 'https:') {
16
+ throw new Error(`${context} must use HTTPS`);
17
+ }
18
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
19
+ throw new Error(
20
+ `${context} must not contain username/password credentials`,
21
+ );
22
+ }
23
+ if (
24
+ parsed.search.length > 0 ||
25
+ parsed.hash.length > 0 ||
26
+ raw.includes('?') ||
27
+ raw.includes('#')
28
+ ) {
29
+ throw new Error(`${context} must not contain a query or fragment`);
30
+ }
31
+ if (parsed.hostname.length === 0) {
32
+ throw new Error(`${context} must identify a repository host`);
33
+ }
34
+ return parsed.toString();
35
+ }
@@ -250,7 +250,8 @@ const SNAPSHOT_PINS_FLAG = '--snapshot-pins';
250
250
 
251
251
  /**
252
252
  * `--snapshot-pins <file>` is the only argument: with it, the plan is the
253
- * committed index applied to that snapshot (the develop edition's frozen pins);
253
+ * develop graph (the data root's firstPartySources) at that snapshot (the
254
+ * develop edition's frozen pins), never the committed index's repositoryPins;
254
255
  * without it, the committed index untouched.
255
256
  */
256
257
  export function indexForCheckout(committedIndex, argv) {
@@ -28,8 +28,8 @@
28
28
  // freeze-graph --data-sha <sha> --kernel-sha <sha>
29
29
  // --deploy-sha <sha> [--editions-root-url <https url>]
30
30
  // [--overrides <json>] --out <file>
31
- // resolve refs/heads/develop once per graph repository (and the editions
32
- // root) from this checkout's runtime-image-source-index.json; the three checked-out
31
+ // resolve refs/heads/develop once per develop-graph repository (the data root's
32
+ // firstPartySources, at each declared URL) and the editions root; the three checked-out
33
33
  // repositories are pinned at what the workflow checked out; write the pins. --kernel-sha is refused
34
34
  // unless its declared edition protocol is one this CLI supports (the check choose-kernel applies to a pin).
35
35
  // check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
@@ -76,7 +76,7 @@ import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
76
76
  import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
77
77
  import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
78
78
  import { EditionChannel } from './edition-constants.mjs';
79
- import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
79
+ import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir } from './edition-workspace.mjs';
80
80
  import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
81
81
  import { assertKernelCommitSupported, chooseKernelCommit } from './kernel-protocol-choice.mjs';
82
82
  import { loadKernelDistributionSchemas } from './lib.mjs';
@@ -187,7 +187,6 @@ async function run(argv, env) {
187
187
  case 'freeze-graph': {
188
188
  assertKernelCommitSupported({ repoDir: kernelRepoDir(), commit: options['--kernel-sha'], label: `--kernel-sha ${options['--kernel-sha']}` });
189
189
  const pins = freezeGraph({
190
- runtimeSourceIndex: readStrictJsonFile(runtimeImageSourceIndexPath()),
191
190
  dataSha: options['--data-sha'],
192
191
  kernelSha: options['--kernel-sha'],
193
192
  deploySha: options['--deploy-sha'],
@@ -15,9 +15,10 @@
15
15
 
16
16
  import { execFileSync } from 'node:child_process';
17
17
 
18
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
19
+ import { developGraphMembers } from './develop-graph.mjs';
18
20
  import { EditionChannel } from './edition-channel.mjs';
19
21
  import { assertCanonicalSourceUrl, assertSourceDeclaresNoCredential, isUnderSourcePrefix, withSourceGitEnv } from './git-source-auth.mjs';
20
- import { canonicalPublicHttpsRepositoryUrl } from './lib.mjs';
21
22
  import {
22
23
  dataPinKey,
23
24
  EDITIONS_ROOT_PIN_KEY,
@@ -144,7 +145,6 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
144
145
 
145
146
  /**
146
147
  * @param {object} request
147
- * @param {object} request.runtimeSourceIndex the checkout's runtime-image-source-index.json (raw)
148
148
  * @param {string} request.dataSha the commit the data root checkout is at
149
149
  * @param {string} request.kernelSha the commit the kernel checkout is at
150
150
  * @param {string} request.deploySha the commit the deploy checkout is at
@@ -152,7 +152,6 @@ function assertCheckedOutPin({ key, sha, flag, what, overrides }) {
152
152
  * @param {(source: {name: string, url: string}) => void} request.holdSource the hold of one source; runs for ALL graph sources before the first head
153
153
  */
154
154
  export function freezeGraph({
155
- runtimeSourceIndex,
156
155
  dataSha,
157
156
  kernelSha,
158
157
  deploySha,
@@ -164,7 +163,9 @@ export function freezeGraph({
164
163
  assertNoToolingOverride(overrides);
165
164
  assertOverrides(overrides, GRAPH_PHASE);
166
165
  const head = memoizedHeads(resolveHead);
167
- const graph = new Set(runtimeSourceIndex.repositoryPins.map((pin) => pin.repo));
166
+ // The develop graph is the data root's first-party repositories (develop-graph.mjs), never the release pins.
167
+ const members = developGraphMembers();
168
+ const graph = new Set(members.map((member) => member.repo));
168
169
  for (const [key] of Object.entries(overrides)) {
169
170
  if (snapshotPinNamespace(key) === SnapshotPinNamespace.Graph && !graph.has(key)) {
170
171
  throw new Error(`override names "${key}", which is not a repository in the graph`);
@@ -176,7 +177,7 @@ export function freezeGraph({
176
177
  { key: deployPinKey(), sha: deploySha, flag: '--deploy-sha', what: 'the deploy checkout' },
177
178
  ];
178
179
  for (const { key, sha, flag, what } of checkedOut) {assertCheckedOutPin({ key, sha, flag, what, overrides });}
179
- const toResolve = runtimeSourceIndex.repositoryPins.map((pin) => ({ name: pin.repo, url: pin.repositoryUrl }));
180
+ const toResolve = members.map((member) => ({ name: member.repo, url: member.repositoryUrl }));
180
181
  if (editionsRootUrl !== undefined) {
181
182
  toResolve.push({ name: EDITIONS_ROOT_PIN_KEY, url: canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url') });
182
183
  }
@@ -186,8 +187,8 @@ export function freezeGraph({
186
187
  throw new Error(`override names "${EDITIONS_ROOT_PIN_KEY}", but the editions root is this repository`);
187
188
  }
188
189
  const pins = Object.fromEntries(checkedOut.map(({ key, sha }) => [key, sha]));
189
- for (const pin of runtimeSourceIndex.repositoryPins) {
190
- pins[pin.repo] = overrides[pin.repo] ?? head({ name: pin.repo, url: pin.repositoryUrl });
190
+ for (const member of members) {
191
+ pins[member.repo] = overrides[member.repo] ?? head({ name: member.repo, url: member.repositoryUrl });
191
192
  }
192
193
  if (editionsRootUrl !== undefined) {
193
194
  const url = canonicalPublicHttpsRepositoryUrl(editionsRootUrl, '--editions-root-url');
@@ -0,0 +1,26 @@
1
+ // develop-graph — which repositories a DEVELOP candidate is built from.
2
+ //
3
+ // The develop graph is every first-party repository the data root declares
4
+ // (`firstPartySources` in edition-workspace.json), each at its declared
5
+ // `repositoryUrl`. It is never read from the release pins
6
+ // (`runtime-image-source-index.json#repositoryPins`): develop and release are
7
+ // separate lifecycles, and a repository is first-party on develop before any
8
+ // release pins it. freeze-graph, the snapshot checkout, the snapshot pins'
9
+ // validation and the snapshot's index all take their members from here.
10
+
11
+ import { firstPartySources } from './first-party-source-ids.mjs';
12
+ import { compareStrings } from './lib.mjs';
13
+
14
+ /**
15
+ * The develop graph: `{ repo, repositoryUrl }` per declared first-party
16
+ * repository, sorted by repo handle.
17
+ *
18
+ * @returns {readonly { repo: string, repositoryUrl: string }[]}
19
+ */
20
+ export function developGraphMembers() {
21
+ return Object.freeze(
22
+ Object.entries(firstPartySources('the develop graph'))
23
+ .map(([repo, { repositoryUrl }]) => Object.freeze({ repo, repositoryUrl }))
24
+ .sort((a, b) => compareStrings(a.repo, b.repo)),
25
+ );
26
+ }
@@ -81,7 +81,6 @@ export async function buildSnapshotInputs({ id, pins, schemas }) {
81
81
  readStrictJsonFile(runtimeImageSourceIndexPath()),
82
82
  );
83
83
  validateSnapshotPins(pins, {
84
- runtimeSourceIndex: committedIndex,
85
84
  declaredSourceNames: composed.declaredSourceNames,
86
85
  editionsRootExternal: editionsRoot() !== dataRoot(),
87
86
  });
@@ -7,7 +7,9 @@
7
7
 
8
8
  import { join } from 'node:path';
9
9
 
10
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
10
11
  import { dataRoot } from './edition-workspace.mjs';
12
+ import { isUnderSourcePrefix } from './git-source-auth.mjs';
11
13
  import { readStrictJsonFile } from './strict-json.mjs';
12
14
  import {
13
15
  parseConfinedBiomes,
@@ -163,6 +165,28 @@ function requireEvidenceTrustPolicy(config) {
163
165
  return Object.freeze({ policy: value.policy, pin: value.pin });
164
166
  }
165
167
 
168
+ /**
169
+ * Every first-party repository URL is canonical and under the workspace's
170
+ * `sourceRepositoryUrlPrefix`: the develop graph sends the source token there.
171
+ */
172
+ function assertFirstPartyUrls(firstPartySources, prefix) {
173
+ for (const [repo, { repositoryUrl }] of Object.entries(firstPartySources)) {
174
+ const where = `${EDITION_WORKSPACE_CONFIG_FILE}: "firstPartySources"["${repo}"].repositoryUrl`;
175
+ let canonical;
176
+ try {
177
+ canonical = canonicalPublicHttpsRepositoryUrl(repositoryUrl, where);
178
+ } catch (err) {
179
+ throw new EditionWorkspaceConfigError(err.message);
180
+ }
181
+ if (canonical !== repositoryUrl) {
182
+ throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(repositoryUrl)} is not in its canonical form (${canonical})`);
183
+ }
184
+ if (!isUnderSourcePrefix(canonical, prefix)) {
185
+ throw new EditionWorkspaceConfigError(`${where} ${JSON.stringify(repositoryUrl)} is not under sourceRepositoryUrlPrefix ${prefix}`);
186
+ }
187
+ }
188
+ }
189
+
166
190
  /** Validate a parsed config document; returns the frozen, typed config. */
167
191
  export function parseEditionWorkspaceConfig(document) {
168
192
  if (document === null || typeof document !== 'object' || Array.isArray(document)) {
@@ -179,11 +203,13 @@ export function parseEditionWorkspaceConfig(document) {
179
203
  .filter(([key]) => document[key] !== undefined)
180
204
  .map(([key, parse]) => [key, parse(document[key])]),
181
205
  );
206
+ const sourceRepositoryUrlPrefix = requireString(document, 'sourceRepositoryUrlPrefix', SOURCE_URL_PREFIX, 'https://<host>/<organisation>/ with a trailing slash');
207
+ if (sections.firstPartySources !== undefined) {assertFirstPartyUrls(sections.firstPartySources, sourceRepositoryUrlPrefix);}
182
208
  return Object.freeze({
183
209
  ...sections,
184
210
  imageRegistry: requireString(document, 'imageRegistry', IMAGE_REGISTRY, '<host>/<namespace> with no scheme or trailing slash'),
185
211
  ...optionalChartSet(document),
186
- sourceRepositoryUrlPrefix: requireString(document, 'sourceRepositoryUrlPrefix', SOURCE_URL_PREFIX, 'https://<host>/<organisation>/ with a trailing slash'),
212
+ sourceRepositoryUrlPrefix,
187
213
  sourceAuthUsername: requireSourceAuthUsername(document),
188
214
  evidenceTrustPolicy: requireEvidenceTrustPolicy(document),
189
215
  });
@@ -15,20 +15,23 @@ import { workspaceSection } from './edition-config.mjs';
15
15
  // `sources[]` entry, so its id is declared in the data root's
16
16
  // `edition-workspace.json` (`firstPartySources`), keyed by the `repo` handle the
17
17
  // runtime image source index pins it under. The key is a lookup handle and may
18
- // move with the aggregator layout; the VALUE is the identity and must not.
18
+ // move with the aggregator layout; the entry's `sourceId` is the identity and
19
+ // must not. The entry's `repositoryUrl` is where the repository lives
20
+ // (provenance, never identity): the develop graph resolves heads there.
19
21
  //
20
- // EVERY repository `runtime-image-source-index.json` pins is declared there (a
21
- // contract test holds the two sets equal), so a newly pinned repository is
22
- // refused at once rather than the day it first declares a composed image. A
23
- // lookup of an undeclared repository is refused (`firstPartySourceId`), never
24
- // named by a fallback.
22
+ // EVERY repository `runtime-image-source-index.json` pins is declared there, at
23
+ // the same URL (the offline index check refuses otherwise), so a newly pinned
24
+ // repository is refused at once rather than the day it first declares a
25
+ // composed image. A declared repository need not be pinned yet: the develop
26
+ // graph carries it before any release does. A lookup of an undeclared
27
+ // repository is refused (`firstPartySourceId`), never named by a fallback.
25
28
 
26
29
  /** The kernel's source-handle grammar (`BiomeSourceNameSchema`): lowercase kebab-case. */
27
30
  export const DECLARED_SOURCE_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
28
31
 
29
- /** The data root's declared id of each first-party repository (`firstPartySources`). */
30
- export const firstPartySourceIds = () =>
31
- workspaceSection('firstPartySources', 'naming a runtime subject after its first-party source');
32
+ /** The data root's first-party repositories (`firstPartySources`): repo handle -> { sourceId, repositoryUrl }. */
33
+ export const firstPartySources = (needed = 'naming a runtime subject after its first-party source') =>
34
+ workspaceSection('firstPartySources', needed);
32
35
 
33
36
  export class UndeclaredSourceIdError extends Error {
34
37
  constructor(message) {
@@ -48,12 +51,12 @@ export function assertDeclaredSourceId(sourceId, where) {
48
51
  }
49
52
 
50
53
  export function firstPartySourceId(repo) {
51
- const ids = firstPartySourceIds();
52
- if (!Object.hasOwn(ids, repo)) {
54
+ const sources = firstPartySources();
55
+ if (!Object.hasOwn(sources, repo)) {
53
56
  throw new UndeclaredSourceIdError(
54
57
  `first-party repository "${repo}" has no declared source id — declare it in ` +
55
58
  'edition-workspace.json under "firstPartySources"; a runtime identity is never derived from a repository name or URL',
56
59
  );
57
60
  }
58
- return assertDeclaredSourceId(ids[repo], `first-party repository "${repo}"`);
61
+ return assertDeclaredSourceId(sources[repo].sourceId, `first-party repository "${repo}"`);
59
62
  }
@@ -25,7 +25,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
25
25
  import { tmpdir } from 'node:os';
26
26
  import { join } from 'node:path';
27
27
 
28
- import { canonicalPublicHttpsRepositoryUrl } from './lib.mjs';
28
+ import { canonicalPublicHttpsRepositoryUrl } from './canonical-repository-url.mjs';
29
29
 
30
30
  /** The fixed environment variable holding the source read token. */
31
31
  export const SOURCE_READ_TOKEN_ENV = 'EVIDENCE_REPOSITORIES_TOKEN';
package/edition/lib.mjs CHANGED
@@ -3075,38 +3075,6 @@ export function validateImmutableImageLock(lock, schemas) {
3075
3075
  return lock;
3076
3076
  }
3077
3077
 
3078
- export function canonicalPublicHttpsRepositoryUrl(raw, context) {
3079
- if (typeof raw !== 'string' || raw.length === 0 || raw.trim() !== raw) {
3080
- throw new Error(`${context} must be a non-empty canonical HTTPS URL`);
3081
- }
3082
- let parsed;
3083
- try {
3084
- parsed = new URL(raw);
3085
- } catch {
3086
- throw new Error(`${context} is not a valid URL`);
3087
- }
3088
- if (parsed.protocol !== 'https:') {
3089
- throw new Error(`${context} must use HTTPS`);
3090
- }
3091
- if (parsed.username.length > 0 || parsed.password.length > 0) {
3092
- throw new Error(
3093
- `${context} must not contain username/password credentials`,
3094
- );
3095
- }
3096
- if (
3097
- parsed.search.length > 0 ||
3098
- parsed.hash.length > 0 ||
3099
- raw.includes('?') ||
3100
- raw.includes('#')
3101
- ) {
3102
- throw new Error(`${context} must not contain a query or fragment`);
3103
- }
3104
- if (parsed.hostname.length === 0) {
3105
- throw new Error(`${context} must identify a repository host`);
3106
- }
3107
- return parsed.toString();
3108
- }
3109
-
3110
3078
  /**
3111
3079
  * Assert that an exact 40-hex commit exists in a source repository. Delegates
3112
3080
  * to `@xemahq/distribution-source-hash` — the tooling holds no second copy of
@@ -10,8 +10,9 @@
10
10
  // published pin. This module owns the SHAPE of that map: a flat object whose
11
11
  // keys live in six closed namespaces and whose values are exact 40-hex SHAs.
12
12
  //
13
- // repos/<x> | submodules/<x> every `repositoryPins[].repo` of the runtime
14
- // image source index (the graph), no more, no fewer
13
+ // repos/<x> | submodules/<x> every repository of the develop graph (the
14
+ // data root's firstPartySources, develop-graph.mjs),
15
+ // no more, no fewer
15
16
  // <pinRoles.data> the commit of the data root checkout (editions and indices)
16
17
  // <pinRoles.kernel> the commit of the kernel the authorization lock is sealed
17
18
  // with (may match repos/<x>, but is NOT in the graph)
@@ -20,6 +21,7 @@
20
21
  // only when that root is NOT this repository
21
22
  // sources/<name> one per `sources[]` entry declared in the chain
22
23
 
24
+ import { developGraphMembers } from './develop-graph.mjs';
23
25
  import { workspaceSection } from './edition-config.mjs';
24
26
  import { compareStrings } from './lib.mjs';
25
27
  import { PinRole } from './workspace-sections.mjs';
@@ -109,19 +111,20 @@ function graphPinsOf(pins) {
109
111
  }
110
112
 
111
113
  /**
112
- * Validate a COMPLETE snapshot against the graph and the composed chain.
113
- * `declaredSourceNames` are the `sources[].name` entries the chain declares;
114
- * `editionsRootExternal` is whether the editions root is another repository.
115
- * Every refusal names the offending key; nothing falls back to a published pin.
114
+ * Validate a COMPLETE snapshot against the develop graph and the composed
115
+ * chain. `declaredSourceNames` are the `sources[].name` entries the chain
116
+ * declares; `editionsRootExternal` is whether the editions root is another
117
+ * repository. Every refusal names the offending key; nothing falls back to a
118
+ * published pin.
116
119
  */
117
120
  export function validateSnapshotPins(
118
121
  pins,
119
- { runtimeSourceIndex, declaredSourceNames, editionsRootExternal },
122
+ { declaredSourceNames, editionsRootExternal },
120
123
  ) {
121
124
  assertPinsObject(pins);
122
125
  assertExactShas(pins);
123
126
  for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
124
- checkGraphPins(runtimeSourceIndex.repositoryPins, pins);
127
+ checkGraphPins(developGraphMembers(), pins);
125
128
  for (const [key, what] of [
126
129
  [dataPinKey(), 'the commit of the data root checkout'],
127
130
  [kernelPinKey(), 'the commit of the kernel the authorization lock is sealed with'],
@@ -175,7 +178,10 @@ export function snapshotPinSource(pins) {
175
178
  /**
176
179
  * Apply a pin source to a parsed runtime image source index. Published returns
177
180
  * the SAME object (inert, byte for byte). Snapshot returns a copy whose
178
- * `repositoryPins` carry the snapshot SHAs.
181
+ * `repositoryPins` are the DEVELOP GRAPH (develop-graph.mjs) at the snapshot
182
+ * SHAs: the committed index's own pins are not read, so a first-party
183
+ * repository no release pins yet is in it, and a pinned one that is not
184
+ * first-party is not.
179
185
  */
180
186
  export function applyPinSource(runtimeSourceIndex, pinSource = PUBLISHED_PIN_SOURCE) {
181
187
  switch (pinSource?.kind) {
@@ -211,12 +217,13 @@ function applySnapshot(runtimeSourceIndex, pins) {
211
217
  assertPinsObject(pins);
212
218
  assertExactShas(pins);
213
219
  for (const key of Object.keys(pins)) {snapshotPinNamespace(key);}
214
- const graph = runtimeSourceIndex.repositoryPins;
220
+ const graph = developGraphMembers();
215
221
  checkGraphPins(graph, pins);
216
222
  return {
217
223
  ...runtimeSourceIndex,
218
- repositoryPins: graph.map((pin) => ({
219
- ...pin,
224
+ repositoryPins: graph.map(({ repo, repositoryUrl }) => ({
225
+ repo,
226
+ repositoryUrl,
220
227
  // A develop snapshot pins every repository at its develop head, so that one
221
228
  // SHA is both the built commit (image axis, `sourceCommit`) and the source
222
229
  // tip (`vendoredSourceCommit`): the image pin lagging the source tip cannot
@@ -226,8 +233,8 @@ function applySnapshot(runtimeSourceIndex, pins) {
226
233
  // The COMMITTED index keeps the axes apart (release channel): see
227
234
  // `source-authority.mjs#publishedSourceTip` and
228
235
  // `build-runtime-image-source-index.mjs#repositoryIdentity`.
229
- vendoredSourceCommit: pins[pin.repo],
230
- sourceCommit: pins[pin.repo],
236
+ sourceCommit: pins[repo],
237
+ vendoredSourceCommit: pins[repo],
231
238
  })),
232
239
  };
233
240
  }
@@ -91,16 +91,46 @@ function supplementalLauncher(value, where) {
91
91
  return Object.freeze({ kind: value.kind, biomeId: text(value.biomeId, `${where}.biomeId`) });
92
92
  }
93
93
 
94
+ /**
95
+ * The retired `{repo handle: source id}` string form of a `firstPartySources`
96
+ * entry. Refused, never read: the entry also declares where the repository
97
+ * lives, which the develop graph resolves its head from.
98
+ */
99
+ export class RetiredFirstPartySourceShapeError extends Error {
100
+ constructor(repo, id) {
101
+ super(
102
+ `${FILE}: "firstPartySources"["${repo}"] is the retired source-id string ${JSON.stringify(id)}. ` +
103
+ `Migrate it to { "sourceId": ${JSON.stringify(id)}, "repositoryUrl": "<the repository's canonical https URL>" }.`,
104
+ );
105
+ this.name = 'RetiredFirstPartySourceShapeError';
106
+ }
107
+ }
108
+
109
+ /**
110
+ * `firstPartySources`: every first-party repository, keyed by its source
111
+ * checkout handle, with its declared source id (the identity a runtime subject
112
+ * is named after) and its repository URL (where it lives: the develop graph
113
+ * resolves its head there, and a release pin must name the same URL). The URL
114
+ * is checked canonical and under `sourceRepositoryUrlPrefix` with the whole
115
+ * config (edition-config.mjs).
116
+ */
94
117
  export function parseFirstPartySources(value) {
95
118
  const where = '"firstPartySources"';
96
- if (!isObject(value) || Object.keys(value).length === 0) {fail(where, 'must be a non-empty {repo handle: source id} object');}
97
- const entries = Object.entries(value).map(([repo, id]) => {
119
+ if (!isObject(value) || Object.keys(value).length === 0) {
120
+ fail(where, 'must be a non-empty {repo handle: { sourceId, repositoryUrl }} object');
121
+ }
122
+ const entries = Object.entries(value).map(([repo, entry]) => {
98
123
  repoHandle(repo, `${where} key`);
99
- if (typeof id !== 'string' || !SOURCE_ID.test(id)) {
100
- fail(`${where}["${repo}"]`, `must be a lowercase kebab-case source id; got ${JSON.stringify(id)}`);
124
+ if (typeof entry === 'string') {throw new RetiredFirstPartySourceShapeError(repo, entry);}
125
+ const at = `${where}["${repo}"]`;
126
+ exactObject(entry, at, ['sourceId', 'repositoryUrl']);
127
+ if (typeof entry.sourceId !== 'string' || !SOURCE_ID.test(entry.sourceId)) {
128
+ fail(`${at}.sourceId`, `must be a lowercase kebab-case source id; got ${JSON.stringify(entry.sourceId)}`);
101
129
  }
102
- return [repo, id];
130
+ return [repo, Object.freeze({ sourceId: entry.sourceId, repositoryUrl: text(entry.repositoryUrl, `${at}.repositoryUrl`) })];
103
131
  });
132
+ uniqueBy(entries, `${where} sourceId`, ([, entry]) => entry.sourceId);
133
+ uniqueBy(entries, `${where} repositoryUrl`, ([, entry]) => entry.repositoryUrl);
104
134
  return Object.freeze(Object.fromEntries(entries));
105
135
  }
106
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:abf054b00865c8a4b0bb44bccd5cc3c65162b8ea8dec47126bc0dfd837392fcd"
95
+ "sourceDigest": "sha256:72008b3d37f2cbcef3c5ec25e2d85989198ebc1f9bc1f481b8b767965813d5aa"
96
96
  }
97
97
  }