@rathnasgala2/schemas 2.11.0 → 2.13.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.
Files changed (408) hide show
  1. package/README.md +192 -84
  2. package/THIRD_PARTY_LICENSES.json +79 -106
  3. package/diagnostics/diagnostic-map.json +443 -405
  4. package/diagnostics/diagnostic-map.public-runtime-origins.json +5 -0
  5. package/docs/catalogs/app-routes.json +2 -1
  6. package/docs/catalogs/schema-inventory.json +33 -54
  7. package/examples/valid/adapter-capability/rules.json +11 -0
  8. package/examples/valid/artifact-manifest/rules.json +156 -177
  9. package/examples/valid/build-input/rules.json +67 -63
  10. package/examples/valid/build-provenance/rules.json +151 -172
  11. package/examples/valid/deployment-intent/rules.json +152 -148
  12. package/examples/valid/deployment-observation/rules.json +24 -20
  13. package/examples/valid/deployment-receipt/rules.json +152 -148
  14. package/examples/valid/navigation/rules.json +2 -4
  15. package/examples/valid/theme-contract/rules.json +4 -1
  16. package/fixtures/boundary/adapter-capability/rules.json +16 -0
  17. package/fixtures/boundary/artifact-manifest/rules.json +195 -202
  18. package/fixtures/boundary/build-input/rules.json +99 -86
  19. package/fixtures/boundary/build-provenance/rules.json +193 -202
  20. package/fixtures/boundary/deployment-intent/rules.json +186 -140
  21. package/fixtures/boundary/deployment-observation/rules.json +68 -18
  22. package/fixtures/boundary/deployment-receipt/rules.json +186 -140
  23. package/fixtures/boundary/navigation/rules.json +2 -17
  24. package/fixtures/boundary/theme-contract/rules.json +34 -0
  25. package/fixtures/invalid/adapter-capability/SCHEMA_STRING_TOO_LONG/cases.json +11 -0
  26. package/fixtures/invalid/adapter-capability/UTF8_BYTE_LENGTH_INVALID/cases.json +11 -0
  27. package/fixtures/invalid/artifact-manifest/ASCII_BYTE_LENGTH_INVALID/cases.json +13 -0
  28. package/fixtures/invalid/artifact-manifest/INTEGER_RANGE_INVALID/cases.json +24 -0
  29. package/fixtures/invalid/artifact-manifest/REQUEST_FIELD_UNKNOWN/cases.json +19 -19
  30. package/fixtures/invalid/artifact-manifest/SCHEMA_CONSTANT_INVALID/cases.json +48 -48
  31. package/fixtures/invalid/artifact-manifest/SCHEMA_ENUM_INVALID/cases.json +11 -11
  32. package/fixtures/invalid/artifact-manifest/SCHEMA_FORMAT_INVALID/cases.json +0 -9
  33. package/fixtures/invalid/artifact-manifest/SCHEMA_NEGATED_RULE_MATCHED/cases.json +0 -11
  34. package/fixtures/invalid/artifact-manifest/SCHEMA_PATTERN_INVALID/cases.json +0 -22
  35. package/fixtures/invalid/artifact-manifest/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +1105 -1105
  36. package/fixtures/invalid/artifact-manifest/SCHEMA_STRING_TOO_LONG/cases.json +4 -6
  37. package/fixtures/invalid/artifact-manifest/SCHEMA_STRING_TOO_SHORT/cases.json +9 -11
  38. package/fixtures/invalid/artifact-manifest/SCHEMA_TYPE_INVALID/cases.json +22 -31
  39. package/fixtures/invalid/artifact-manifest/UTF8_BYTE_LENGTH_INVALID/cases.json +0 -11
  40. package/fixtures/invalid/build-input/INTEGER_RANGE_INVALID/cases.json +13 -0
  41. package/fixtures/invalid/build-input/REQUEST_FIELD_UNKNOWN/cases.json +32 -32
  42. package/fixtures/invalid/build-input/SCHEMA_CONSTANT_INVALID/cases.json +11 -11
  43. package/fixtures/invalid/build-input/SCHEMA_ENUM_INVALID/cases.json +11 -11
  44. package/fixtures/invalid/build-input/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +78 -78
  45. package/fixtures/invalid/build-input/SCHEMA_TYPE_INVALID/cases.json +51 -18
  46. package/fixtures/invalid/build-provenance/ASCII_BYTE_LENGTH_INVALID/cases.json +13 -0
  47. package/fixtures/invalid/build-provenance/INTEGER_RANGE_INVALID/cases.json +22 -0
  48. package/fixtures/invalid/build-provenance/REQUEST_FIELD_UNKNOWN/cases.json +56 -56
  49. package/fixtures/invalid/build-provenance/SCHEMA_CONSTANT_INVALID/cases.json +37 -37
  50. package/fixtures/invalid/build-provenance/SCHEMA_ENUM_INVALID/cases.json +11 -11
  51. package/fixtures/invalid/build-provenance/SCHEMA_FORMAT_INVALID/cases.json +0 -9
  52. package/fixtures/invalid/build-provenance/SCHEMA_NEGATED_RULE_MATCHED/cases.json +0 -11
  53. package/fixtures/invalid/build-provenance/SCHEMA_PATTERN_INVALID/cases.json +0 -22
  54. package/fixtures/invalid/build-provenance/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +927 -927
  55. package/fixtures/invalid/build-provenance/SCHEMA_STRING_TOO_LONG/cases.json +9 -11
  56. package/fixtures/invalid/build-provenance/SCHEMA_STRING_TOO_SHORT/cases.json +8 -10
  57. package/fixtures/invalid/build-provenance/SCHEMA_TYPE_INVALID/cases.json +20 -25
  58. package/fixtures/invalid/build-provenance/UTF8_BYTE_LENGTH_INVALID/cases.json +0 -11
  59. package/fixtures/invalid/deployment-intent/INTEGER_RANGE_INVALID/cases.json +24 -0
  60. package/fixtures/invalid/deployment-intent/REQUEST_FIELD_UNKNOWN/cases.json +30 -30
  61. package/fixtures/invalid/deployment-intent/SCHEMA_CONSTANT_INVALID/cases.json +41 -41
  62. package/fixtures/invalid/deployment-intent/SCHEMA_ENUM_INVALID/cases.json +11 -11
  63. package/fixtures/invalid/deployment-intent/SCHEMA_FORMAT_INVALID/cases.json +0 -9
  64. package/fixtures/invalid/deployment-intent/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +1209 -1209
  65. package/fixtures/invalid/deployment-intent/SCHEMA_STRING_TOO_LONG/cases.json +9 -0
  66. package/fixtures/invalid/deployment-intent/SCHEMA_STRING_TOO_SHORT/cases.json +9 -0
  67. package/fixtures/invalid/deployment-intent/SCHEMA_TYPE_INVALID/cases.json +22 -20
  68. package/fixtures/invalid/deployment-observation/INTEGER_RANGE_INVALID/cases.json +24 -0
  69. package/fixtures/invalid/deployment-observation/REQUEST_FIELD_UNKNOWN/cases.json +16 -16
  70. package/fixtures/invalid/deployment-observation/SCHEMA_FORMAT_INVALID/cases.json +0 -9
  71. package/fixtures/invalid/deployment-observation/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +39 -39
  72. package/fixtures/invalid/deployment-observation/SCHEMA_STRING_TOO_LONG/cases.json +11 -0
  73. package/fixtures/invalid/deployment-observation/SCHEMA_STRING_TOO_SHORT/cases.json +11 -0
  74. package/fixtures/invalid/deployment-observation/SCHEMA_TYPE_INVALID/cases.json +11 -11
  75. package/fixtures/invalid/deployment-receipt/INTEGER_RANGE_INVALID/cases.json +24 -0
  76. package/fixtures/invalid/deployment-receipt/REQUEST_FIELD_UNKNOWN/cases.json +70 -70
  77. package/fixtures/invalid/deployment-receipt/SCHEMA_CONSTANT_INVALID/cases.json +36 -36
  78. package/fixtures/invalid/deployment-receipt/SCHEMA_ENUM_INVALID/cases.json +11 -11
  79. package/fixtures/invalid/deployment-receipt/SCHEMA_FORMAT_INVALID/cases.json +0 -9
  80. package/fixtures/invalid/deployment-receipt/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +1304 -1304
  81. package/fixtures/invalid/deployment-receipt/SCHEMA_STRING_TOO_LONG/cases.json +9 -0
  82. package/fixtures/invalid/deployment-receipt/SCHEMA_STRING_TOO_SHORT/cases.json +9 -0
  83. package/fixtures/invalid/deployment-receipt/SCHEMA_TYPE_INVALID/cases.json +22 -20
  84. package/fixtures/invalid/navigation/SCHEMA_ARRAY_TOO_LONG/cases.json +0 -15
  85. package/fixtures/invalid/navigation/SCHEMA_CONSTANT_INVALID/cases.json +11 -0
  86. package/fixtures/invalid/navigation/SCHEMA_TYPE_INVALID/cases.json +0 -11
  87. package/fixtures/invalid/theme-contract/INTEGER_RANGE_INVALID/cases.json +11 -0
  88. package/fixtures/invalid/theme-contract/SCHEMA_FORMAT_INVALID/cases.json +0 -9
  89. package/fixtures/invalid/theme-contract/SCHEMA_STRING_TOO_LONG/cases.json +9 -0
  90. package/fixtures/invalid/theme-contract/SCHEMA_STRING_TOO_SHORT/cases.json +9 -0
  91. package/fixtures/manifest.json +3217 -2930
  92. package/fixtures/unknown-field/adapter-capability/rules.json +2 -0
  93. package/fixtures/unknown-field/artifact-manifest/rules.json +111 -114
  94. package/fixtures/unknown-field/build-input/rules.json +36 -32
  95. package/fixtures/unknown-field/build-provenance/rules.json +111 -114
  96. package/fixtures/unknown-field/deployment-intent/rules.json +109 -105
  97. package/fixtures/unknown-field/deployment-observation/rules.json +24 -20
  98. package/fixtures/unknown-field/deployment-receipt/rules.json +109 -105
  99. package/fixtures/unknown-field/navigation/rules.json +1 -3
  100. package/fixtures/unknown-field/theme-contract/rules.json +3 -0
  101. package/generated/browser/runtime-origins-validator-core.d.mts +10 -0
  102. package/generated/browser/runtime-origins-validator-core.mjs +35 -0
  103. package/generated/browser/validator-core.d.mts +10 -0
  104. package/generated/browser/validator-core.mjs +80 -0
  105. package/generated/typescript/contracts/artifact-manifest.d.ts +49 -48
  106. package/generated/typescript/contracts/build-input.d.ts +15 -15
  107. package/generated/typescript/contracts/build-provenance.d.ts +51 -50
  108. package/generated/typescript/contracts/deployment-intent.d.ts +6 -6
  109. package/generated/typescript/contracts/deployment-observation.d.ts +1 -1
  110. package/generated/typescript/contracts/deployment-receipt.d.ts +5 -5
  111. package/generated/typescript/contracts/navigation.d.ts +1 -1
  112. package/generated/typescript/index.js +11 -37
  113. package/openapi/http-catalog.json +14 -2
  114. package/openapi/openapi.yaml +527 -13
  115. package/package.json +19 -16
  116. package/sbom.cdx.json +23 -23
  117. package/schemas/adapter-capability.schema.json +5 -1
  118. package/schemas/appearance.schema.json +1 -1
  119. package/schemas/artifact-manifest.schema.json +15 -18
  120. package/schemas/build-input.schema.json +18 -11
  121. package/schemas/build-provenance.schema.json +15 -18
  122. package/schemas/deployment-intent.schema.json +13 -8
  123. package/schemas/deployment-observation.schema.json +10 -5
  124. package/schemas/deployment-receipt.schema.json +12 -7
  125. package/schemas/navigation.schema.json +5 -26
  126. package/schemas/theme-contract.schema.json +4 -2
  127. package/src/index.js +2 -2
  128. package/src/internal/browser-schema-validator.js +43 -0
  129. package/src/internal/format-validators.js +1 -1
  130. package/src/internal/gala-keywords.js +148 -0
  131. package/src/internal/validator-core.js +244 -108
  132. package/src/runtime-origins.js +18 -18
  133. package/types/index.d.ts +2 -2
  134. package/types/internal/browser-schema-validator.d.ts +16 -0
  135. package/types/internal/format-validators.d.ts +1 -1
  136. package/types/internal/gala-keywords.d.ts +68 -0
  137. package/types/internal/validator-core.d.ts +80 -1
  138. package/compatibility/compatibility.json +0 -594
  139. package/generated/java/src/main/java/io/gala/schema/generated/GalaSchemaRegistry.java +0 -67
  140. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityAdapterConfigurationCapabilities.java +0 -24
  141. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityAdapterIdentity.java +0 -20
  142. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityCapabilityDecision.java +0 -62
  143. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityCredentialEgressProfile.java +0 -35
  144. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityDestinationIdentity.java +0 -25
  145. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityDestinationProviderCoordinates.java +0 -19
  146. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityDocument.java +0 -52
  147. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityFilesystemProviderLimits.java +0 -32
  148. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityGithubActionsOidcOriginCatalog.java +0 -25
  149. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityHttpProviderLimits.java +0 -72
  150. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemAllowlist.java +0 -21
  151. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemAllowlistEntry.java +0 -28
  152. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemCapabilityEvidence.java +0 -42
  153. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemControlPublication.java +0 -26
  154. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemControlRow.java +0 -44
  155. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemFailureEvidence.java +0 -40
  156. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemObservationEvidence.java +0 -40
  157. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemPlatform.java +0 -25
  158. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemProbeSummary.java +0 -32
  159. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemProbeTranscript.java +0 -25
  160. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemReplacement.java +0 -22
  161. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemSurfaceIdentity.java +0 -20
  162. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityManagedExecutionBudget.java +0 -26
  163. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityPagesOidcCredentialEgress.java +0 -30
  164. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderCallClassBinding.java +0 -22
  165. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderCredentialEgress.java +0 -26
  166. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderCredentialHeader.java +0 -24
  167. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderDerivedHeader.java +0 -18
  168. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderFixedHeader.java +0 -18
  169. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderRequestTemplate.java +0 -39
  170. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneBinding.java +0 -32
  171. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneRequest.java +0 -26
  172. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneRequestCatalog.java +0 -31
  173. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneResponseCatalog.java +0 -20
  174. package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesWebsiteConfiguration.java +0 -24
  175. package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceColorMode.java +0 -19
  176. package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceDocument.java +0 -37
  177. package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceExtensionMap.java +0 -15
  178. package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceSemanticTokens.java +0 -15
  179. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestActionPinEvidence.java +0 -20
  180. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestArtifactLicenseConclusion.java +0 -21
  181. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestAssertedWorkload.java +0 -44
  182. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestBuildSandboxEvidence.java +0 -38
  183. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestDocument.java +0 -73
  184. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestFinding.java +0 -22
  185. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestAsset.java +0 -24
  186. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestCompositionIdentity.java +0 -25
  187. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestExcludedInput.java +0 -22
  188. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestIncludedSource.java +0 -22
  189. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestRedirect.java +0 -24
  190. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestRoute.java +0 -28
  191. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestSourceIdentity.java +0 -26
  192. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestValidationEvidence.java +0 -22
  193. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestMeasurement.java +0 -20
  194. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestPackageIdentity.java +0 -22
  195. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestRenderPolicyIdentity.java +0 -20
  196. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestReproducibleBuildRecord.java +0 -56
  197. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestWorkflowCarrierEvidence.java +0 -26
  198. package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestWorkflowFileEvidence.java +0 -26
  199. package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorDocument.java +0 -33
  200. package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorExtensionMap.java +0 -15
  201. package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorLocalizedAuthor.java +0 -20
  202. package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorSocialLink.java +0 -19
  203. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputAdapterIdentity.java +0 -20
  204. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputAppearanceNormalized.java +0 -33
  205. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputAuthorNormalized.java +0 -31
  206. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputBuildPackages.java +0 -25
  207. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputColorMode.java +0 -19
  208. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputContentBuildRecord.java +0 -33
  209. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputContentFrontmatterNormalized.java +0 -44
  210. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputDestinationCapabilityProfile.java +0 -20
  211. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputDocument.java +0 -51
  212. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputLocalizedAuthor.java +0 -20
  213. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputModuleBuildSelection.java +0 -15
  214. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputNavigationItem.java +0 -23
  215. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputNavigationLeaf.java +0 -22
  216. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputNavigationNormalized.java +0 -21
  217. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputPackageIdentity.java +0 -22
  218. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputPublicationNormalized.java +0 -39
  219. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputRenderPolicyIdentity.java +0 -20
  220. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputRenderableBody.java +0 -26
  221. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputRepositorySnapshot.java +0 -22
  222. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputResolvedFile.java +0 -18
  223. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputResolvedMedia.java +0 -20
  224. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputSemanticTokens.java +0 -15
  225. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputSocialLink.java +0 -19
  226. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceActionPinEvidence.java +0 -20
  227. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceArtifactLicenseConclusion.java +0 -21
  228. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceAssertedWorkload.java +0 -44
  229. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceBuildSandboxEvidence.java +0 -38
  230. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceDocument.java +0 -69
  231. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceFinding.java +0 -22
  232. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestAsset.java +0 -24
  233. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestCompositionIdentity.java +0 -25
  234. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestExcludedInput.java +0 -22
  235. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestIncludedSource.java +0 -22
  236. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestRedirect.java +0 -24
  237. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestRoute.java +0 -28
  238. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestSourceIdentity.java +0 -26
  239. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestValidationEvidence.java +0 -22
  240. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceMeasurement.java +0 -20
  241. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenancePackageIdentity.java +0 -22
  242. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceRenderPolicyIdentity.java +0 -20
  243. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceReproducibleBuildRecord.java +0 -56
  244. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceWorkflowCarrierEvidence.java +0 -26
  245. package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceWorkflowFileEvidence.java +0 -26
  246. package/generated/java/src/main/java/io/gala/schema/generated/model/ContentFrontmatterDocument.java +0 -50
  247. package/generated/java/src/main/java/io/gala/schema/generated/model/ContentFrontmatterExtensionMap.java +0 -15
  248. package/generated/java/src/main/java/io/gala/schema/generated/model/ContentFrontmatterMediaRef.java +0 -20
  249. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationBasis.java +0 -20
  250. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationDetectionObservation.java +0 -33
  251. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationDetectionPlan.java +0 -44
  252. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationDetectionProbe.java +0 -52
  253. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentAdapterIdentity.java +0 -20
  254. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentCancellationFinalizationEvidence.java +0 -41
  255. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDeadlineFinalizationEvidence.java +0 -39
  256. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDeadlineFinalizationStream.java +0 -22
  257. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDeploymentPolicyDecision.java +0 -75
  258. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDestinationIdentity.java +0 -25
  259. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDestinationMutationAuthority.java +0 -36
  260. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDestinationProviderCoordinates.java +0 -19
  261. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDocument.java +0 -136
  262. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentExpectedRedirectHop.java +0 -23
  263. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentObservedVerificationHeader.java +0 -19
  264. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPackageIdentity.java +0 -22
  265. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesInterveningRunAttempt.java +0 -20
  266. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesReconciliationCommand.java +0 -48
  267. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesReconciliationRecovery.java +0 -41
  268. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesRunAttemptGapProof.java +0 -27
  269. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPublicGenerationMarkerPayload.java +0 -24
  270. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPublicProbeObservation.java +0 -54
  271. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentRecognizedPriorRouteContract.java +0 -30
  272. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentRenderPolicyIdentity.java +0 -20
  273. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentReproducibleBuildRecord.java +0 -56
  274. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentSupersessionFinalizationEvidence.java +0 -45
  275. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentVerificationHeaderExpectation.java +0 -18
  276. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentVerificationPlanTarget.java +0 -52
  277. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationActivationDetectionObservation.java +0 -33
  278. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationActivationDetectionProbe.java +0 -52
  279. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationAdapterIdentity.java +0 -20
  280. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationCancellationFinalizationEvidence.java +0 -41
  281. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDeadlineFinalizationEvidence.java +0 -39
  282. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDeadlineFinalizationStream.java +0 -22
  283. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDestinationIdentity.java +0 -25
  284. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDestinationProviderCoordinates.java +0 -19
  285. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDocument.java +0 -57
  286. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationExpectedRedirectHop.java +0 -23
  287. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationObservedVerificationHeader.java +0 -19
  288. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationPackageIdentity.java +0 -22
  289. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationPublicProbeObservation.java +0 -54
  290. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationRecognizedPriorRouteContract.java +0 -30
  291. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationRenderPolicyIdentity.java +0 -20
  292. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationSupersessionFinalizationEvidence.java +0 -45
  293. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationVerificationHeaderExpectation.java +0 -18
  294. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationVerificationPlanTarget.java +0 -52
  295. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationBasis.java +0 -20
  296. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationDetectionObservation.java +0 -33
  297. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationDetectionPlan.java +0 -44
  298. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationDetectionProbe.java +0 -52
  299. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptAdapterIdentity.java +0 -20
  300. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptCancellationFinalizationEvidence.java +0 -41
  301. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeadlineFinalizationEvidence.java +0 -39
  302. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeadlineFinalizationStream.java +0 -22
  303. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentAttempt.java +0 -38
  304. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentFailure.java +0 -24
  305. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentObservation.java +0 -53
  306. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentPolicyDecision.java +0 -75
  307. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageEvidence.java +0 -36
  308. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageInput.java +0 -26
  309. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageObservationEvidenceReference.java +0 -18
  310. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageResult.java +0 -31
  311. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDestinationIdentity.java +0 -25
  312. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDestinationMutationAuthority.java +0 -36
  313. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDestinationProviderCoordinates.java +0 -19
  314. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDocument.java +0 -105
  315. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptExpectedRedirectHop.java +0 -23
  316. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptFinding.java +0 -22
  317. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptObservedVerificationHeader.java +0 -19
  318. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPackageIdentity.java +0 -22
  319. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesInterveningRunAttempt.java +0 -20
  320. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesReconciliationCommand.java +0 -48
  321. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesReconciliationRecovery.java +0 -41
  322. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesRunAttemptGapProof.java +0 -27
  323. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPublicGenerationMarkerPayload.java +0 -24
  324. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPublicProbeObservation.java +0 -54
  325. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptRecognizedPriorRouteContract.java +0 -30
  326. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptRenderPolicyIdentity.java +0 -20
  327. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptReproducibleBuildRecord.java +0 -56
  328. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptSupersessionFinalizationEvidence.java +0 -45
  329. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptVerificationHeaderExpectation.java +0 -18
  330. package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptVerificationPlanTarget.java +0 -52
  331. package/generated/java/src/main/java/io/gala/schema/generated/model/EventEnvelopeDocument.java +0 -46
  332. package/generated/java/src/main/java/io/gala/schema/generated/model/EventEnvelopeRegisteredEventPayload.java +0 -26
  333. package/generated/java/src/main/java/io/gala/schema/generated/model/LockDependencyEdge.java +0 -18
  334. package/generated/java/src/main/java/io/gala/schema/generated/model/LockDocument.java +0 -39
  335. package/generated/java/src/main/java/io/gala/schema/generated/model/LockLockedPackage.java +0 -26
  336. package/generated/java/src/main/java/io/gala/schema/generated/model/LockLockedTemplate.java +0 -28
  337. package/generated/java/src/main/java/io/gala/schema/generated/model/LockPackageIdentity.java +0 -22
  338. package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationDocument.java +0 -25
  339. package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationExtensionMap.java +0 -15
  340. package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationNavigationItem.java +0 -23
  341. package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationNavigationLeaf.java +0 -23
  342. package/generated/java/src/main/java/io/gala/schema/generated/model/ProblemDocument.java +0 -37
  343. package/generated/java/src/main/java/io/gala/schema/generated/model/ProblemFieldError.java +0 -18
  344. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicGenerationMarkerDocument.java +0 -24
  345. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicRuntimeOriginsDocument.java +0 -44
  346. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationDocument.java +0 -41
  347. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationExtensionMap.java +0 -15
  348. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationFooterCard.java +0 -23
  349. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationPublicationProfile.java +0 -18
  350. package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationSocialLink.java +0 -19
  351. package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryAssetRoot.java +0 -16
  352. package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryContentRoot.java +0 -23
  353. package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryDocument.java +0 -41
  354. package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryExtensionMap.java +0 -15
  355. package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryGeneratedRoot.java +0 -20
  356. package/generated/java/src/main/java/io/gala/schema/generated/model/TemplateCompositionDocument.java +0 -18
  357. package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractDocument.java +0 -53
  358. package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractPassiveAsset.java +0 -24
  359. package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractThemeBudgets.java +0 -20
  360. package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractThemeToken.java +0 -22
  361. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/adapter-capability.schema.json +0 -2689
  362. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/appearance.schema.json +0 -226
  363. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/artifact-manifest.schema.json +0 -1063
  364. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/author.schema.json +0 -231
  365. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/build-input.schema.json +0 -866
  366. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/build-provenance.schema.json +0 -937
  367. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/content-frontmatter.schema.json +0 -241
  368. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/deployment-intent.schema.json +0 -2067
  369. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/deployment-observation.schema.json +0 -1654
  370. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/deployment-receipt.schema.json +0 -5546
  371. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/event-envelope.schema.json +0 -195
  372. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/lock.schema.json +0 -326
  373. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/navigation.schema.json +0 -229
  374. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/problem.schema.json +0 -194
  375. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/public-generation-marker.schema.json +0 -142
  376. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/public-runtime-origins.schema.json +0 -183
  377. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/publication.schema.json +0 -248
  378. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/repository.schema.json +0 -219
  379. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/template-composition.schema.json +0 -133
  380. package/generated/java/src/main/resources/io/gala/schema/generated/schemas/theme-contract.schema.json +0 -850
  381. package/generated/typescript/validator-core.cjs +0 -17
  382. package/openapi/generator/spring.json +0 -22
  383. package/openapi/generator/typescript-fetch.json +0 -19
  384. package/openapi/source/authentication.yaml +0 -557
  385. package/openapi/source/callbacks.yaml +0 -332
  386. package/openapi/source/components.yaml +0 -5670
  387. package/openapi/source/github.yaml +0 -351
  388. package/openapi/source/internal.yaml +0 -71
  389. package/openapi/source/membership-invitations.yaml +0 -203
  390. package/openapi/source/organizations.yaml +0 -11144
  391. package/openapi/source/root.yaml +0 -20
  392. package/openapi/source/self.yaml +0 -1940
  393. package/openapi/source/session.yaml +0 -308
  394. package/openapi/source/workloads.yaml +0 -744
  395. package/src/internal/build-artifact-semantics.js +0 -4391
  396. package/src/internal/deployment-record-semantics.js +0 -7567
  397. package/src/internal/deployment-stage-semantics.js +0 -1207
  398. package/src/internal/http-problem-contract.js +0 -35
  399. package/src/internal/managed-evidence-journal.js +0 -160
  400. package/src/internal/public-runtime-origins.js +0 -143
  401. package/src/internal/theme-composition-semantics.js +0 -2441
  402. package/types/internal/build-artifact-semantics.d.ts +0 -77
  403. package/types/internal/deployment-record-semantics.d.ts +0 -313
  404. package/types/internal/deployment-stage-semantics.d.ts +0 -108
  405. package/types/internal/http-problem-contract.d.ts +0 -4
  406. package/types/internal/managed-evidence-journal.d.ts +0 -26
  407. package/types/internal/public-runtime-origins.d.ts +0 -22
  408. package/types/internal/theme-composition-semantics.d.ts +0 -143
@@ -1,4391 +0,0 @@
1
- import {
2
- createHash,
3
- createPublicKey,
4
- verify as verifySignature,
5
- } from 'node:crypto';
6
-
7
- import {
8
- canonicalizeJcs,
9
- canonicalizeJcsBytes,
10
- parseDuplicateFreeIJson,
11
- sha256Tagged,
12
- } from './canonical-jcs.js';
13
- import {
14
- ACTIVE_DIGEST_PROFILES,
15
- digestActionDefinitionBlob,
16
- digestRenderPolicyBytes,
17
- } from './digest-profiles.js';
18
- import { validateFrozenEnvelope } from './frozen-envelope.js';
19
- import { validateRfc3339 } from './portable-scalars.js';
20
- import {
21
- parseSemver,
22
- parseSemverRange,
23
- satisfiesSemverRange,
24
- } from './semver.js';
25
- import { validateSpdxCatalogEvidence, validateSpdxExpression } from './spdx.js';
26
- import unicodeTable from './generated/unicode17.json' with { type: 'json' };
27
- import {
28
- assertUnicodeScalarString,
29
- graphemeLength17,
30
- normalizeNfc17,
31
- unicodeCollisionKey17,
32
- } from './unicode17.js';
33
-
34
- const FATAL_UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
35
- const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
36
- const GIT_OBJECT_ID_PATTERN = /^(sha1:[0-9a-f]{40}|sha256:[0-9a-f]{64})$/u;
37
- const GITHUB_SHA_PATTERN = /^[0-9a-f]{40}$/u;
38
- const POSITIVE_DECIMAL_PATTERN = /^[1-9][0-9]*$/u;
39
- const NONNEGATIVE_DECIMAL_PATTERN = /^(?:0|[1-9][0-9]*)$/u;
40
- const STABLE_ID_PATTERN =
41
- /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
42
- const NPM_PACKAGE_PATTERN =
43
- /^(?:[a-z0-9][a-z0-9._-]*|@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*)$/u;
44
- const MAX_INT64 = 9_223_372_036_854_775_807n;
45
- const MAX_PACKAGE_BYTES = 134_217_728n;
46
- const MAX_SOURCE_FILE_BYTES = 10_485_760;
47
- const MAX_SOURCE_BYTES = 536_870_912n;
48
- const MAX_ARTIFACT_FILES = 50_000;
49
- const MAX_ARTIFACT_BYTES = 1_073_741_824n;
50
- const SPDX_SCHEMA_DIGEST =
51
- 'sha256:239208b7ac287b3cf5d9a9af23f9d69863971102a5e1587a27a398b43490b89b';
52
-
53
- const THEME_TO_ID = /** @type {Readonly<Record<string, string>>} */ (
54
- Object.freeze({
55
- '@rathnasgala2/theme-default': 'default',
56
- '@rathnasgala2/theme-amaze': 'amaze',
57
- '@rathnasgala2/theme-flashy': 'flashy',
58
- '@rathnasgala2/theme-minimal': 'minimal',
59
- '@rathnasgala2/theme-zebra': 'zebra',
60
- })
61
- );
62
-
63
- const ADAPTER_TO_ID = /** @type {Readonly<Record<string, string>>} */ (
64
- Object.freeze({
65
- '@rathnasgala2/adapter-local-directory': 'local-directory',
66
- '@rathnasgala2/adapter-github-pages': 'github-pages',
67
- '@rathnasgala2/adapter-do-spaces': 'do-spaces',
68
- })
69
- );
70
-
71
- const DIRECT_PACKAGE_NAMES = Object.freeze([
72
- '@rathnasgala2/schemas',
73
- '@rathnasgala2/template',
74
- null,
75
- '@rathnasgala2/publish-action',
76
- '@rathnasgala2/publish-kernel',
77
- '@rathnasgala2/adapter-protocol',
78
- null,
79
- ]);
80
-
81
- const OIDC_CLAIMS = Object.freeze([
82
- 'actor',
83
- 'actor_id',
84
- 'aud',
85
- 'event_name',
86
- 'iss',
87
- 'job_workflow_ref',
88
- 'job_workflow_sha',
89
- 'jti',
90
- 'ref',
91
- 'repository',
92
- 'repository_id',
93
- 'repository_owner',
94
- 'repository_owner_id',
95
- 'run_attempt',
96
- 'run_id',
97
- 'run_number',
98
- 'runner_environment',
99
- 'sha',
100
- 'sub',
101
- 'workflow_ref',
102
- 'workflow_sha',
103
- ]);
104
-
105
- const WORKFLOW_ROLES = Object.freeze([
106
- 'author-caller',
107
- 'publish',
108
- 'authorize',
109
- 'report',
110
- ]);
111
- const WORKFLOW_PATHS = Object.freeze([
112
- '.github/workflows/gala-publish-v2.yml',
113
- '.github/workflows/publish-v2.yml',
114
- '.github/workflows/authorize-v2.yml',
115
- '.github/workflows/report-v2.yml',
116
- ]);
117
- const SOURCE_ROLES = Object.freeze([
118
- 'publication',
119
- 'author',
120
- 'content',
121
- 'navigation',
122
- 'appearance',
123
- 'asset',
124
- ]);
125
- const MANIFEST_MEDIA_TYPES = Object.freeze([
126
- 'text/html; charset=utf-8',
127
- 'text/css; charset=utf-8',
128
- 'text/plain; charset=utf-8',
129
- 'application/javascript; charset=utf-8',
130
- 'application/json; charset=utf-8',
131
- 'application/manifest+json; charset=utf-8',
132
- 'application/atom+xml; charset=utf-8',
133
- 'application/rss+xml; charset=utf-8',
134
- 'application/xml; charset=utf-8',
135
- 'image/png',
136
- 'image/jpeg',
137
- 'image/webp',
138
- 'image/avif',
139
- 'image/svg+xml',
140
- 'font/woff2',
141
- 'application/octet-stream',
142
- ]);
143
-
144
- const LOCK_KEYS = Object.freeze([
145
- 'schemaId',
146
- 'schemaVersion',
147
- 'repositorySchemaVersion',
148
- 'resolvedAt',
149
- 'resolver',
150
- 'schemas',
151
- 'template',
152
- 'theme',
153
- 'publisher',
154
- 'dependencies',
155
- 'dependencyDag',
156
- 'lockDigest',
157
- ]);
158
- const BUILD_INPUT_KEYS = Object.freeze([
159
- 'schemaId',
160
- 'schemaVersion',
161
- 'contractVersion',
162
- 'repository',
163
- 'sourceRevision',
164
- 'packages',
165
- 'publication',
166
- 'authors',
167
- 'content',
168
- 'navigation',
169
- 'appearance',
170
- 'modules',
171
- 'buildEpoch',
172
- 'baseUrl',
173
- 'basePath',
174
- 'destinationCapabilities',
175
- 'placements',
176
- 'inputDigest',
177
- ]);
178
- const MANIFEST_KEYS = Object.freeze([
179
- 'schemaId',
180
- 'schemaVersion',
181
- 'repositoryNodeId',
182
- 'sourceCommit',
183
- 'workflowIdentity',
184
- 'buildToolVersions',
185
- 'buildInputDigest',
186
- 'artifactDigest',
187
- 'routes',
188
- 'assets',
189
- 'findings',
190
- 'measurements',
191
- 'policyResult',
192
- 'generatedAt',
193
- 'reproducibilityClass',
194
- 'artifactId',
195
- 'artifactFileCount',
196
- 'artifactByteCount',
197
- 'sourceIdentity',
198
- 'builder',
199
- 'composition',
200
- 'buildInputContractVersion',
201
- 'redirects',
202
- 'declarativeHeaders',
203
- 'includedSources',
204
- 'excludedInputs',
205
- 'sourceInventoryDigest',
206
- 'validation',
207
- 'manifestDigest',
208
- ]);
209
- const PROVENANCE_KEYS = Object.freeze([
210
- 'schemaId',
211
- 'schemaVersion',
212
- 'assertedWorkload',
213
- 'requiredOidcClaims',
214
- 'workflowFiles',
215
- 'actionPins',
216
- 'verifiedInputHandoff',
217
- 'unfrozenOutputHandoff',
218
- 'rebuildRecord',
219
- 'lockDigest',
220
- 'packageReleaseCatalogDigest',
221
- 'buildInputDigest',
222
- 'artifactId',
223
- 'artifactDigest',
224
- 'manifestDigest',
225
- 'sbomDigest',
226
- 'spdxLicenseListVersion',
227
- 'spdxLicenseListDigest',
228
- 'spdx23JsonSchemaDigest',
229
- 'artifactLicenseConclusions',
230
- 'policyReleaseId',
231
- 'buildPolicyDecisionDigest',
232
- 'capabilityDecisionDigest',
233
- 'stylingContractDigest',
234
- 'renderPolicy',
235
- 'sandbox',
236
- 'secretInputs',
237
- ]);
238
-
239
- /**
240
- * Facts that the semantic validator deliberately receives from trusted owners.
241
- * None of these facts is added to a portable record.
242
- */
243
- export const BUILD_ARTIFACT_CONTEXT_CONSTRAINTS = Object.freeze([
244
- 'Author-source parsing, reference resolution, route derivation, and normalized-field projection must complete before context construction.',
245
- 'Git transport must obtain the selected commit and exact object bytes, derive sourceTree and buildEpoch, enumerate the complete source tree, and validate refs; this validator rehashes and resolves each retained action commit/tree/blob proof.',
246
- 'Registry request/response transport, DNS/TLS limits, retained packument metadata, Sigstore admission, tar/gzip safety, package extraction, lifecycle-field exclusion, and external-import scans remain prep-owned.',
247
- 'OIDC discovery/JWKS transport, cache single-flight behavior, and atomic jti replay consumption remain authorization-owned; this validator verifies the retained profile, key projection, compact JWS, claims, clocks, and binding.',
248
- 'The selected render-policy catalog and deterministic renderer behavior remain template-owned; this validator verifies the exact retained policy bytes and every identity/body digest copy.',
249
- 'Exclusion-rule catalog membership, renderer output completeness, output media classification, and execution in two distinct empty roots must be supplied as immutable evidence; this validator checks their complete projections, partition, bytes, and equality.',
250
- 'The workflow owner must validate the verified-input and unfrozen-output carrier codecs and content closures before context construction; this validator rechecks their retained REST identities, exact bytes, lengths, digests, purposes, and names.',
251
- 'GitHub runner release admission and carrier upload/download/one-day-expiry observations remain workflow-owned; this validator checks the retained compatibility row, executable bytes, and evidence copies.',
252
- 'The pinned official SPDX 2.3 JSON Schema must independently accept the exact retained SBOM bytes; this validator enforces the stricter Gala projection and the pinned schema-byte digest.',
253
- 'Optional GitHub attestations and provider upload observations remain downstream owners and must use byte-identical frozen-envelope provenance and SBOM records.',
254
- ]);
255
-
256
- /** Stable semantic validation failure. */
257
- export class BuildArtifactSemanticError extends TypeError {
258
- /**
259
- * @param {string} code stable diagnostic code
260
- */
261
- constructor(code) {
262
- super(code);
263
- this.name = 'BuildArtifactSemanticError';
264
- this.code = code;
265
- }
266
- }
267
-
268
- /**
269
- * @param {string} code stable diagnostic code
270
- * @returns {never} always throws
271
- */
272
- function fail(code) {
273
- throw new BuildArtifactSemanticError(code);
274
- }
275
-
276
- /**
277
- * @param {unknown} value candidate object
278
- * @param {string} code diagnostic code
279
- * @returns {Record<string, unknown>} plain object
280
- */
281
- function asObject(value, code) {
282
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
283
- fail(code);
284
- }
285
- const prototype = Object.getPrototypeOf(value);
286
- if (prototype !== Object.prototype && prototype !== null) fail(code);
287
- return /** @type {Record<string, unknown>} */ (value);
288
- }
289
-
290
- /**
291
- * @param {unknown} value candidate array
292
- * @param {string} code diagnostic code
293
- * @returns {unknown[]} array
294
- */
295
- function asArray(value, code) {
296
- if (!Array.isArray(value)) fail(code);
297
- return value;
298
- }
299
-
300
- /**
301
- * @param {unknown} value candidate object
302
- * @param {readonly string[]} required exact member ledger
303
- * @param {string} code diagnostic code
304
- * @param {readonly string[]} [optional] optional members
305
- * @returns {Record<string, unknown>} validated object
306
- */
307
- function ledger(value, required, code, optional = []) {
308
- const object = asObject(value, code);
309
- const allowed = new Set([...required, ...optional]);
310
- const keys = Object.keys(object);
311
- if (
312
- required.some((key) => !Object.hasOwn(object, key)) ||
313
- keys.some((key) => !allowed.has(key))
314
- ) {
315
- fail(code);
316
- }
317
- return object;
318
- }
319
-
320
- /**
321
- * @param {unknown} value candidate string
322
- * @param {string} code diagnostic code
323
- * @returns {string} string
324
- */
325
- function asString(value, code) {
326
- if (typeof value !== 'string') fail(code);
327
- return value;
328
- }
329
-
330
- /**
331
- * @param {unknown} actual candidate value
332
- * @param {unknown} expected exact expected value
333
- * @param {string} code diagnostic code
334
- * @returns {void}
335
- */
336
- function equal(actual, expected, code) {
337
- if (actual !== expected) fail(code);
338
- }
339
-
340
- /**
341
- * @param {unknown} actual candidate JSON value
342
- * @param {unknown} expected exact expected JSON value
343
- * @param {string} code diagnostic code
344
- * @returns {void}
345
- */
346
- function jcsEqual(actual, expected, code) {
347
- try {
348
- if (canonicalizeJcs(actual) !== canonicalizeJcs(expected)) fail(code);
349
- } catch (error) {
350
- if (error instanceof BuildArtifactSemanticError) throw error;
351
- fail(code);
352
- }
353
- }
354
-
355
- /**
356
- * @param {unknown} value candidate digest
357
- * @param {string} code diagnostic code
358
- * @returns {string} digest
359
- */
360
- function digest(value, code) {
361
- const source = asString(value, code);
362
- if (!DIGEST_PATTERN.test(source)) fail(code);
363
- return source;
364
- }
365
-
366
- /**
367
- * @param {unknown} value candidate Git object ID
368
- * @param {string} code diagnostic code
369
- * @returns {string} object ID
370
- */
371
- function gitObjectId(value, code) {
372
- const source = asString(value, code);
373
- if (!GIT_OBJECT_ID_PATTERN.test(source)) fail(code);
374
- return source;
375
- }
376
-
377
- /**
378
- * @param {unknown} value raw GitHub SHA claim
379
- * @param {string} code diagnostic code
380
- * @returns {string} tagged Git object ID
381
- */
382
- function githubSha(value, code) {
383
- const source = asString(value, code);
384
- if (!GITHUB_SHA_PATTERN.test(source)) fail(code);
385
- return `sha1:${source}`;
386
- }
387
-
388
- /**
389
- * @param {unknown} value canonical GitHub positive decimal
390
- * @param {string} code diagnostic code
391
- * @returns {string} validated decimal
392
- */
393
- function githubPositiveDecimal(value, code) {
394
- const source = asString(value, code);
395
- if (
396
- source.length > 20 ||
397
- !POSITIVE_DECIMAL_PATTERN.test(source) ||
398
- BigInt(source) > 18_446_744_073_709_551_615n
399
- ) {
400
- fail(code);
401
- }
402
- return source;
403
- }
404
-
405
- /**
406
- * @param {unknown} value canonical UUIDv7
407
- * @param {string} code diagnostic code
408
- * @returns {string} validated identifier
409
- */
410
- function stableId(value, code) {
411
- const source = asString(value, code);
412
- if (!STABLE_ID_PATTERN.test(source)) fail(code);
413
- return source;
414
- }
415
-
416
- /**
417
- * @param {unknown} value canonical npm package name
418
- * @param {string} code diagnostic code
419
- * @returns {string} validated package name
420
- */
421
- function npmPackageName(value, code) {
422
- const source = asString(value, code);
423
- if (
424
- Buffer.byteLength(source, 'utf8') > 214 ||
425
- !NPM_PACKAGE_PATTERN.test(source)
426
- ) {
427
- fail(code);
428
- }
429
- const components = source.startsWith('@')
430
- ? source.slice(1).split('/')
431
- : [source];
432
- if (components.some((component) => component === '.' || component === '..')) {
433
- fail(code);
434
- }
435
- return source;
436
- }
437
-
438
- /**
439
- * @param {unknown} value exact GitHub repository coordinate
440
- * @param {string} code diagnostic code
441
- * @returns {{coordinate: string, owner: string, repository: string}} parsed coordinate
442
- */
443
- function githubRepositoryCoordinate(value, code) {
444
- const coordinate = asString(value, code);
445
- if (
446
- Buffer.byteLength(coordinate, 'ascii') !==
447
- Buffer.byteLength(coordinate, 'utf8') ||
448
- Buffer.byteLength(coordinate, 'ascii') < 3 ||
449
- Buffer.byteLength(coordinate, 'ascii') > 140
450
- ) {
451
- fail(code);
452
- }
453
- const parts = coordinate.split('/');
454
- const owner = parts[0];
455
- const repository = parts[1];
456
- if (
457
- parts.length !== 2 ||
458
- !owner ||
459
- !repository ||
460
- Buffer.byteLength(owner, 'ascii') > 39 ||
461
- Buffer.byteLength(repository, 'ascii') > 100 ||
462
- !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/u.test(owner) ||
463
- !/^[A-Za-z0-9._-]+$/u.test(repository) ||
464
- repository === '.' ||
465
- repository === '..'
466
- ) {
467
- fail(code);
468
- }
469
- return { coordinate, owner, repository };
470
- }
471
-
472
- /**
473
- * @param {unknown} value candidate bytes
474
- * @param {string} code diagnostic code
475
- * @returns {Buffer} exact byte view
476
- */
477
- function bytes(value, code) {
478
- if (!(value instanceof Uint8Array)) fail(code);
479
- return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
480
- }
481
-
482
- /**
483
- * @param {unknown} value canonical nonnegative int64
484
- * @param {string} code diagnostic code
485
- * @returns {bigint} parsed value
486
- */
487
- function nonnegativeInt64(value, code) {
488
- const source = asString(value, code);
489
- if (!NONNEGATIVE_DECIMAL_PATTERN.test(source)) fail(code);
490
- const parsed = BigInt(source);
491
- if (parsed > MAX_INT64) fail(code);
492
- return parsed;
493
- }
494
-
495
- /**
496
- * @param {unknown} value canonical positive int64
497
- * @param {string} code diagnostic code
498
- * @returns {bigint} parsed value
499
- */
500
- function positiveInt64(value, code) {
501
- const source = asString(value, code);
502
- if (!POSITIVE_DECIMAL_PATTERN.test(source)) fail(code);
503
- const parsed = BigInt(source);
504
- if (parsed > MAX_INT64) fail(code);
505
- return parsed;
506
- }
507
-
508
- /**
509
- * @param {unknown} value canonical timestamp
510
- * @param {string} code diagnostic code
511
- * @returns {string} timestamp
512
- */
513
- function timestamp(value, code) {
514
- const source = asString(value, code);
515
- try {
516
- validateRfc3339(source);
517
- } catch {
518
- fail(code);
519
- }
520
- return source;
521
- }
522
-
523
- /**
524
- * @param {unknown} value canonical semantic version
525
- * @param {string} code diagnostic code
526
- * @returns {string} version
527
- */
528
- function semver(value, code) {
529
- const source = asString(value, code);
530
- try {
531
- parseSemver(source);
532
- } catch {
533
- fail(code);
534
- }
535
- return source;
536
- }
537
-
538
- /**
539
- * @param {unknown} value canonical semantic-version range
540
- * @param {string} code diagnostic code
541
- * @returns {string} range
542
- */
543
- function semverRange(value, code) {
544
- const source = asString(value, code);
545
- try {
546
- parseSemverRange(source);
547
- } catch {
548
- fail(code);
549
- }
550
- return source;
551
- }
552
-
553
- /**
554
- * Require normalized bounded plain-label text.
555
- *
556
- * @param {unknown} value candidate label
557
- * @param {string} code diagnostic code
558
- * @returns {string} checked label
559
- */
560
- function plainLabel(value, code) {
561
- const source = asString(value, code);
562
- try {
563
- assertUnicodeScalarString(source);
564
- } catch {
565
- fail(code);
566
- }
567
- if (
568
- normalizeNfc17(source) !== source ||
569
- graphemeLength17(source) < 1 ||
570
- graphemeLength17(source) > 80 ||
571
- source.includes('<') ||
572
- source.includes('>') ||
573
- [...source].some((character) => {
574
- const codePoint = character.codePointAt(0) ?? 0;
575
- return (
576
- codePoint <= 0x1f ||
577
- (codePoint >= 0x7f && codePoint <= 0x9f) ||
578
- (codePoint >= 0x202a && codePoint <= 0x202e) ||
579
- (codePoint >= 0x2066 && codePoint <= 0x2069)
580
- );
581
- })
582
- ) {
583
- fail(code);
584
- }
585
- return source;
586
- }
587
-
588
- /**
589
- * @param {string} left left UTF-8 string
590
- * @param {string} right right UTF-8 string
591
- * @returns {number} byte ordering
592
- */
593
- function compareUtf8(left, right) {
594
- return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
595
- }
596
-
597
- /**
598
- * @param {unknown} left left JSON value
599
- * @param {unknown} right right JSON value
600
- * @returns {number} JCS member ordering
601
- */
602
- function compareJcs(left, right) {
603
- return Buffer.compare(
604
- canonicalizeJcsBytes(left),
605
- canonicalizeJcsBytes(right),
606
- );
607
- }
608
-
609
- /**
610
- * @param {unknown[]} values candidate canonical set
611
- * @param {string} code diagnostic code
612
- * @param {(left: unknown, right: unknown) => number} [compare] ordering
613
- * @returns {void}
614
- */
615
- function assertCanonicalSet(values, code, compare = compareJcs) {
616
- for (let index = 1; index < values.length; index += 1) {
617
- const prior = values[index - 1];
618
- const current = values[index];
619
- if (
620
- prior === undefined ||
621
- current === undefined ||
622
- compare(prior, current) >= 0
623
- ) {
624
- fail(code);
625
- }
626
- }
627
- }
628
-
629
- /**
630
- * @param {string} name active digest profile name
631
- * @param {unknown} value complete owner value
632
- * @param {string} code diagnostic code
633
- * @returns {string} computed digest
634
- */
635
- function digestProfile(name, value, code) {
636
- const profile = ACTIVE_DIGEST_PROFILES[name];
637
- if (!profile) fail(code);
638
- try {
639
- return profile.digest(value);
640
- } catch {
641
- fail(code);
642
- }
643
- }
644
-
645
- /**
646
- * @param {Uint8Array} value exact bytes
647
- * @param {'sha1' | 'sha256'} algorithm algorithm
648
- * @returns {string} lowercase hash
649
- */
650
- function rawHash(value, algorithm) {
651
- return createHash(algorithm).update(value).digest('hex');
652
- }
653
-
654
- /**
655
- * @param {Record<string, unknown>} value package record
656
- * @param {string} code diagnostic code
657
- * @returns {{package: string, version: string, integrity: string, registry: string}} identity projection
658
- */
659
- function packageIdentity(value, code) {
660
- const packageName = npmPackageName(value.package, code);
661
- const version = semver(value.version, code);
662
- const integrity = digest(value.integrity, code);
663
- const registry = asString(value.registry, code);
664
- return { package: packageName, version, integrity, registry };
665
- }
666
-
667
- /**
668
- * @param {{package: string, version: string}} identity package identity
669
- * @returns {string} exact package coordinate
670
- */
671
- function packageKey(identity) {
672
- return `${identity.package}@${identity.version}`;
673
- }
674
-
675
- /**
676
- * @param {string} path candidate path
677
- * @returns {string} normalized collision key
678
- */
679
- function validatePortablePath(path) {
680
- assertUnicodeScalarString(path);
681
- if (
682
- path.length === 0 ||
683
- Buffer.byteLength(path, 'utf8') > 512 ||
684
- normalizeNfc17(path) !== path ||
685
- path.startsWith('/') ||
686
- path.endsWith('/') ||
687
- path.includes('//')
688
- ) {
689
- fail('PORTABLE_PATH_INVALID');
690
- }
691
- const properties =
692
- /** @type {{whiteSpace: [number, number, string][], noncharacter: [number, number, string][], defaultIgnorable: [number, number, string][]}} */ (
693
- /** @type {unknown} */ (unicodeTable)
694
- );
695
- const reserved = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|com[¹²³]|lpt[¹²³])$/u;
696
- for (const segment of path.split('/')) {
697
- const scalars = [...segment];
698
- const first = scalars[0];
699
- const last = scalars.at(-1);
700
- if (
701
- segment === '.' ||
702
- segment === '..' ||
703
- Buffer.byteLength(segment, 'utf8') > 128 ||
704
- first === undefined ||
705
- last === undefined ||
706
- last === '.'
707
- ) {
708
- fail('PORTABLE_PATH_INVALID');
709
- }
710
- const firstCodePoint = first.codePointAt(0);
711
- const lastCodePoint = last.codePointAt(0);
712
- if (
713
- firstCodePoint === undefined ||
714
- lastCodePoint === undefined ||
715
- inRanges(firstCodePoint, properties.whiteSpace) ||
716
- inRanges(lastCodePoint, properties.whiteSpace)
717
- ) {
718
- fail('PORTABLE_PATH_INVALID');
719
- }
720
- for (const character of scalars) {
721
- const codePoint = character.codePointAt(0);
722
- if (
723
- codePoint === undefined ||
724
- codePoint < 0x20 ||
725
- (codePoint >= 0x7f && codePoint <= 0x9f) ||
726
- ['%', '\\', '<', '>', ':', '"', '|', '?', '*'].includes(character) ||
727
- inRanges(codePoint, properties.defaultIgnorable) ||
728
- inRanges(codePoint, properties.noncharacter)
729
- ) {
730
- fail('PORTABLE_PATH_INVALID');
731
- }
732
- }
733
- const stem = segment.split('.', 1)[0];
734
- if (stem && reserved.test(unicodeCollisionKey17(stem))) {
735
- fail('PORTABLE_PATH_INVALID');
736
- }
737
- }
738
- return unicodeCollisionKey17(path);
739
- }
740
-
741
- /**
742
- * @param {number} codePoint Unicode scalar
743
- * @param {[number, number, string][]} ranges sorted ranges
744
- * @returns {boolean} membership
745
- */
746
- function inRanges(codePoint, ranges) {
747
- let low = 0;
748
- let high = ranges.length - 1;
749
- while (low <= high) {
750
- const middle = Math.floor((low + high) / 2);
751
- const range = ranges[middle];
752
- if (!range) return false;
753
- if (codePoint < range[0]) high = middle - 1;
754
- else if (codePoint > range[1]) low = middle + 1;
755
- else return true;
756
- }
757
- return false;
758
- }
759
-
760
- /**
761
- * @param {string} objectId tagged Git object ID
762
- * @param {'blob' | 'commit' | 'tree'} type Git object type
763
- * @param {Uint8Array} content exact object bytes
764
- * @returns {boolean} whether bytes produce the object ID
765
- */
766
- function gitObjectMatches(objectId, type, content) {
767
- const separator = objectId.indexOf(':');
768
- const algorithm = objectId.slice(0, separator);
769
- if (algorithm !== 'sha1' && algorithm !== 'sha256') return false;
770
- const header = Buffer.from(`${type} ${content.byteLength}\0`, 'utf8');
771
- const actual = createHash(algorithm)
772
- .update(header)
773
- .update(content)
774
- .digest('hex');
775
- return objectId.slice(separator + 1) === actual;
776
- }
777
-
778
- /**
779
- * @param {string} objectId tagged Git blob ID
780
- * @param {Uint8Array} content exact blob bytes
781
- * @returns {boolean} whether bytes produce the object ID
782
- */
783
- function gitBlobMatches(objectId, content) {
784
- return gitObjectMatches(objectId, 'blob', content);
785
- }
786
-
787
- /**
788
- * @param {Record<string, unknown>} profile retained registry profile
789
- * @returns {void}
790
- */
791
- function validateRegistryFetchProfile(profile) {
792
- const code = 'LOCK_REGISTRY_PROFILE_INVALID';
793
- ledger(
794
- profile,
795
- [
796
- 'profile',
797
- 'origin',
798
- 'method',
799
- 'httpVersion',
800
- 'requestTargetSource',
801
- 'requestHeaders',
802
- 'requestBodyBytes',
803
- 'acceptedStatus',
804
- 'acceptedContentType',
805
- 'maximumResponseHeadBytes',
806
- 'maximumTarballBytes',
807
- 'maximumResponseWireBodyBytes',
808
- 'requestTimeoutMillis',
809
- 'redirects',
810
- 'retries',
811
- 'ambientProxy',
812
- 'netrc',
813
- 'cookies',
814
- 'credentials',
815
- 'networkBoundaryProfileDigest',
816
- 'tlsProfileDigest',
817
- 'fixtureDigest',
818
- 'evidenceDigest',
819
- 'profileDigest',
820
- ],
821
- code,
822
- );
823
- const fixed = {
824
- profile: 'gala-npm-registry-fetch-v2',
825
- origin: 'https://registry.npmjs.org',
826
- method: 'GET',
827
- httpVersion: 'HTTP/1.1',
828
- requestTargetSource: 'catalog-entry-tarball-path',
829
- requestHeaders: [
830
- 'accept: application/octet-stream',
831
- 'accept-encoding: identity',
832
- 'connection: close',
833
- 'host: registry.npmjs.org',
834
- ],
835
- requestBodyBytes: 0,
836
- acceptedStatus: 200,
837
- acceptedContentType: 'application/octet-stream',
838
- maximumResponseHeadBytes: 32_768,
839
- maximumTarballBytes: 134_217_728,
840
- maximumResponseWireBodyBytes: 805_306_373,
841
- requestTimeoutMillis: 300_000,
842
- redirects: 'reject',
843
- retries: 0,
844
- ambientProxy: 'disabled',
845
- netrc: 'disabled',
846
- cookies: 'disabled',
847
- credentials: [],
848
- };
849
- for (const [key, expected] of Object.entries(fixed)) {
850
- jcsEqual(profile[key], expected, code);
851
- }
852
- for (const key of [
853
- 'networkBoundaryProfileDigest',
854
- 'tlsProfileDigest',
855
- 'fixtureDigest',
856
- 'evidenceDigest',
857
- 'profileDigest',
858
- ]) {
859
- digest(profile[key], code);
860
- }
861
- equal(
862
- profile.profileDigest,
863
- digestProfile('npmRegistryFetchProfile', profile, code),
864
- code,
865
- );
866
- }
867
-
868
- /**
869
- * @param {string} path catalog tarball request target
870
- * @returns {void}
871
- */
872
- function validateTarballPath(path) {
873
- if (
874
- Buffer.byteLength(path, 'ascii') !== Buffer.byteLength(path, 'utf8') ||
875
- Buffer.byteLength(path, 'ascii') > 2_048 ||
876
- !path.startsWith('/') ||
877
- path.endsWith('/') ||
878
- path.includes('//') ||
879
- /[%\\?#]/u.test(path)
880
- ) {
881
- fail('LOCK_CATALOG_INVALID');
882
- }
883
- const segments = path.slice(1).split('/');
884
- if (
885
- segments.length < 2 ||
886
- segments.length > 32 ||
887
- segments.some(
888
- (segment) =>
889
- segment.length > 255 ||
890
- segment === '.' ||
891
- segment === '..' ||
892
- !/^[A-Za-z0-9@._~+-]+$/u.test(segment),
893
- )
894
- ) {
895
- fail('LOCK_CATALOG_INVALID');
896
- }
897
- }
898
-
899
- /**
900
- * Resolve the only admitted first-party package-name families.
901
- *
902
- * @param {string} packageName canonical npm package name
903
- * @param {string} code diagnostic code
904
- * @returns {string} exact source repository coordinate
905
- */
906
- function firstPartySourceRepository(packageName, code) {
907
- const suffix = packageName.slice('@rathnasgala2/'.length);
908
- if (suffix === 'schemas') return 'rathnasgala2/schema';
909
- if (suffix === 'template') return 'rathnasgala2/template';
910
- if (Object.hasOwn(THEME_TO_ID, packageName)) {
911
- return `rathnasgala2/${suffix}`;
912
- }
913
- if (
914
- packageName === '@rathnasgala2/publish-action' ||
915
- packageName === '@rathnasgala2/publish-kernel' ||
916
- packageName === '@rathnasgala2/adapter-protocol' ||
917
- Object.hasOwn(ADAPTER_TO_ID, packageName)
918
- ) {
919
- return 'rathnasgala2/publish';
920
- }
921
- fail(code);
922
- }
923
-
924
- /**
925
- * @param {Record<string, unknown>} entry catalog entry
926
- * @returns {void}
927
- */
928
- function validateCatalogEntry(entry) {
929
- const code = 'LOCK_CATALOG_INVALID';
930
- const baseKeys = [
931
- 'package',
932
- 'version',
933
- 'integrity',
934
- 'tarballPath',
935
- 'tarballByteCount',
936
- 'authorityKind',
937
- 'licenseExpression',
938
- 'approvalEvidenceDigest',
939
- ];
940
- const provenanceKeys = [
941
- 'sourceRepository',
942
- 'sourceRepositoryId',
943
- 'sourceCommit',
944
- 'sourceRef',
945
- 'releaseWorkflowPath',
946
- 'releaseWorkflowCommit',
947
- 'provenanceBundleDigest',
948
- ];
949
- const firstParty = asString(entry.package, code).startsWith('@rathnasgala2/');
950
- ledger(entry, firstParty ? [...baseKeys, ...provenanceKeys] : baseKeys, code);
951
- const packageName = npmPackageName(entry.package, code);
952
- semver(entry.version, code);
953
- digest(entry.integrity, code);
954
- const tarballPath = asString(entry.tarballPath, code);
955
- validateTarballPath(tarballPath);
956
- const byteCount = positiveInt64(entry.tarballByteCount, code);
957
- if (byteCount > MAX_PACKAGE_BYTES) fail(code);
958
- try {
959
- validateSpdxExpression(asString(entry.licenseExpression, code));
960
- } catch {
961
- fail(code);
962
- }
963
- digest(entry.approvalEvidenceDigest, code);
964
- if (firstParty) {
965
- equal(entry.authorityKind, 'rathnasgala-npm-provenance', code);
966
- const expectedRepository = firstPartySourceRepository(packageName, code);
967
- equal(entry.sourceRepository, expectedRepository, code);
968
- githubRepositoryCoordinate(entry.sourceRepository, code);
969
- githubPositiveDecimal(entry.sourceRepositoryId, code);
970
- gitObjectId(entry.sourceCommit, code);
971
- const sourceRef = asString(entry.sourceRef, code);
972
- if (
973
- !sourceRef.startsWith('refs/tags/') ||
974
- sourceRef.length < 11 ||
975
- sourceRef.length > 512
976
- )
977
- fail(code);
978
- equal(entry.releaseWorkflowPath, '.github/workflows/release.yaml', code);
979
- gitObjectId(entry.releaseWorkflowCommit, code);
980
- digest(entry.provenanceBundleDigest, code);
981
- } else {
982
- equal(entry.authorityKind, 'approved-upstream', code);
983
- }
984
- }
985
-
986
- /**
987
- * @param {Record<string, unknown>} metadata retained runtime metadata
988
- * @param {Map<string, Record<string, unknown>>} identities complete locked closure
989
- * @returns {{from: string, to: string}[]} derived dependency edges
990
- */
991
- function validateRuntimeMetadata(metadata, identities) {
992
- const code = 'LOCK_RUNTIME_METADATA_INVALID';
993
- ledger(metadata, ['name', 'version', 'license', 'requirements'], code, [
994
- 'enginesNode',
995
- ]);
996
- const name = npmPackageName(metadata.name, code);
997
- const version = semver(metadata.version, code);
998
- try {
999
- validateSpdxExpression(asString(metadata.license, code));
1000
- } catch {
1001
- fail(code);
1002
- }
1003
- if (Object.hasOwn(metadata, 'enginesNode')) {
1004
- const engine = semverRange(metadata.enginesNode, code);
1005
- if (!satisfiesSemverRange('24.18.0', engine)) fail(code);
1006
- }
1007
- const requirements = asArray(metadata.requirements, code).map((candidate) =>
1008
- ledger(candidate, ['kind', 'package', 'range'], code),
1009
- );
1010
- if (requirements.length > 256) fail(code);
1011
- assertCanonicalSet(requirements, code, (left, right) => {
1012
- const a = asObject(left, code);
1013
- const b = asObject(right, code);
1014
- const kindComparison = compareUtf8(
1015
- asString(a.kind, code),
1016
- asString(b.kind, code),
1017
- );
1018
- return (
1019
- kindComparison ||
1020
- compareUtf8(asString(a.package, code), asString(b.package, code))
1021
- );
1022
- });
1023
- const seenPackages = new Set();
1024
- const edges = [];
1025
- for (const requirement of requirements) {
1026
- const kind = asString(requirement.kind, code);
1027
- if (kind !== 'dependency' && kind !== 'peer') fail(code);
1028
- const packageName = npmPackageName(requirement.package, code);
1029
- if (seenPackages.has(packageName)) fail(code);
1030
- seenPackages.add(packageName);
1031
- const range = semverRange(requirement.range, code);
1032
- const candidates = [...identities.values()].filter(
1033
- (identity) => identity.package === packageName,
1034
- );
1035
- if (candidates.length !== 1) fail(code);
1036
- const selected = candidates[0];
1037
- if (
1038
- !selected ||
1039
- !satisfiesSemverRange(asString(selected.version, code), range)
1040
- ) {
1041
- fail(code);
1042
- }
1043
- edges.push({
1044
- from: `${name}@${version}`,
1045
- to: packageKey(packageIdentity(selected, code)),
1046
- });
1047
- }
1048
- return edges;
1049
- }
1050
-
1051
- /**
1052
- * @typedef {{
1053
- * direct: Record<string, unknown>[],
1054
- * all: Record<string, unknown>[],
1055
- * identities: Map<string, Record<string, unknown>>,
1056
- * catalogByKey: Map<string, Record<string, unknown>>,
1057
- * artifactsByKey: Map<string, Record<string, unknown>>,
1058
- * catalog: Record<string, unknown>,
1059
- * adapterPackage: Record<string, unknown>
1060
- * }} LockState
1061
- */
1062
-
1063
- /**
1064
- * Validate exact resolution, catalog, bytes, runtime graph, and build-package projection.
1065
- *
1066
- * @param {Record<string, unknown>} lock lock record
1067
- * @param {Record<string, unknown>} buildInput build-input record
1068
- * @param {Record<string, unknown>} context trusted validator context
1069
- * @returns {LockState} validated package state
1070
- */
1071
- function validateLock(lock, buildInput, context) {
1072
- const ledgerCode = 'LOCK_LEDGER_INVALID';
1073
- ledger(lock, LOCK_KEYS, ledgerCode);
1074
- equal(lock.schemaId, 'urn:gala:schema:lock:2.0.0', ledgerCode);
1075
- equal(lock.schemaVersion, '2.0.0', ledgerCode);
1076
- semver(lock.repositorySchemaVersion, ledgerCode);
1077
- timestamp(lock.resolvedAt, ledgerCode);
1078
- const resolver = asObject(lock.resolver, ledgerCode);
1079
- const schemas = asObject(lock.schemas, ledgerCode);
1080
- const template = asObject(lock.template, ledgerCode);
1081
- const theme = asObject(lock.theme, ledgerCode);
1082
- const publishers = asArray(lock.publisher, 'LOCK_ROLE_INVALID').map((row) =>
1083
- asObject(row, 'LOCK_ROLE_INVALID'),
1084
- );
1085
- if (publishers.length !== 4) fail('LOCK_ROLE_INVALID');
1086
- const adapterPackage = publishers[3];
1087
- if (!adapterPackage) fail('LOCK_ROLE_INVALID');
1088
- const actualNames = [
1089
- asString(schemas.package, 'LOCK_ROLE_INVALID'),
1090
- asString(template.package, 'LOCK_ROLE_INVALID'),
1091
- asString(theme.package, 'LOCK_ROLE_INVALID'),
1092
- ...publishers.map((row) => asString(row.package, 'LOCK_ROLE_INVALID')),
1093
- ];
1094
- const expectedNames = [...DIRECT_PACKAGE_NAMES];
1095
- expectedNames[2] = Object.hasOwn(THEME_TO_ID, actualNames[2] ?? '')
1096
- ? (actualNames[2] ?? null)
1097
- : null;
1098
- expectedNames[6] = Object.hasOwn(ADAPTER_TO_ID, actualNames[6] ?? '')
1099
- ? (actualNames[6] ?? null)
1100
- : null;
1101
- if (actualNames.some((name, index) => name !== expectedNames[index])) {
1102
- fail('LOCK_ROLE_INVALID');
1103
- }
1104
- for (const row of [schemas, template, theme, ...publishers]) {
1105
- packageIdentity(row, 'LOCK_ROLE_INVALID');
1106
- semver(row.contractVersion, 'LOCK_ROLE_INVALID');
1107
- semverRange(row.compatibleWith, 'LOCK_ROLE_INVALID');
1108
- }
1109
- jcsEqual(
1110
- packageIdentity(resolver, 'LOCK_ROLE_INVALID'),
1111
- packageIdentity(publishers[0] ?? {}, 'LOCK_ROLE_INVALID'),
1112
- 'LOCK_ROLE_INVALID',
1113
- );
1114
- equal(resolver.package, '@rathnasgala2/publish-action', 'LOCK_ROLE_INVALID');
1115
- const dependencies = asArray(lock.dependencies, 'LOCK_DAG_INVALID').map(
1116
- (row) => asObject(row, 'LOCK_DAG_INVALID'),
1117
- );
1118
- if (dependencies.length > 512) fail('LOCK_DAG_INVALID');
1119
- for (const row of dependencies) packageIdentity(row, 'LOCK_DAG_INVALID');
1120
- assertCanonicalSet(dependencies, 'LOCK_DAG_INVALID', (left, right) => {
1121
- const a = asObject(left, 'LOCK_DAG_INVALID');
1122
- const b = asObject(right, 'LOCK_DAG_INVALID');
1123
- const packageComparison = compareUtf8(
1124
- asString(a.package, 'LOCK_DAG_INVALID'),
1125
- asString(b.package, 'LOCK_DAG_INVALID'),
1126
- );
1127
- return (
1128
- packageComparison ||
1129
- compareUtf8(
1130
- asString(a.version, 'LOCK_DAG_INVALID'),
1131
- asString(b.version, 'LOCK_DAG_INVALID'),
1132
- )
1133
- );
1134
- });
1135
- const direct = [schemas, template, theme, ...publishers];
1136
- const all = [...direct, ...dependencies];
1137
- const identities = new Map();
1138
- const names = new Map();
1139
- for (const row of all) {
1140
- const identity = packageIdentity(row, 'LOCK_DAG_INVALID');
1141
- const key = packageKey(identity);
1142
- if (identities.has(key) || names.has(identity.package))
1143
- fail('LOCK_DAG_INVALID');
1144
- identities.set(key, row);
1145
- names.set(identity.package, identity.version);
1146
- }
1147
-
1148
- const catalog = ledger(
1149
- context.packageReleaseCatalog,
1150
- [
1151
- 'profile',
1152
- 'catalogId',
1153
- 'registry',
1154
- 'registryFetchProfileDigest',
1155
- 'sigstoreTrustRootDigest',
1156
- 'entries',
1157
- 'catalogDigest',
1158
- ],
1159
- 'LOCK_CATALOG_INVALID',
1160
- );
1161
- equal(
1162
- catalog.profile,
1163
- 'gala-package-release-catalog-v2',
1164
- 'LOCK_CATALOG_INVALID',
1165
- );
1166
- stableId(catalog.catalogId, 'LOCK_CATALOG_INVALID');
1167
- equal(
1168
- catalog.registry,
1169
- 'https://registry.npmjs.org/',
1170
- 'LOCK_CATALOG_INVALID',
1171
- );
1172
- digest(catalog.sigstoreTrustRootDigest, 'LOCK_CATALOG_INVALID');
1173
- const catalogEntries = asArray(catalog.entries, 'LOCK_CATALOG_INVALID').map(
1174
- (row) => asObject(row, 'LOCK_CATALOG_INVALID'),
1175
- );
1176
- if (catalogEntries.length < 1 || catalogEntries.length > 4_096) {
1177
- fail('LOCK_CATALOG_INVALID');
1178
- }
1179
- for (const entry of catalogEntries) validateCatalogEntry(entry);
1180
- assertCanonicalSet(catalogEntries, 'LOCK_CATALOG_INVALID', (left, right) => {
1181
- const a = asObject(left, 'LOCK_CATALOG_INVALID');
1182
- const b = asObject(right, 'LOCK_CATALOG_INVALID');
1183
- const packageComparison = compareUtf8(
1184
- asString(a.package, 'LOCK_CATALOG_INVALID'),
1185
- asString(b.package, 'LOCK_CATALOG_INVALID'),
1186
- );
1187
- return (
1188
- packageComparison ||
1189
- compareUtf8(
1190
- asString(a.version, 'LOCK_CATALOG_INVALID'),
1191
- asString(b.version, 'LOCK_CATALOG_INVALID'),
1192
- )
1193
- );
1194
- });
1195
- equal(
1196
- catalog.catalogDigest,
1197
- digestProfile('packageReleaseCatalog', catalog, 'LOCK_CATALOG_INVALID'),
1198
- 'LOCK_CATALOG_INVALID',
1199
- );
1200
- const registryProfile = asObject(
1201
- context.registryFetchProfile,
1202
- 'LOCK_REGISTRY_PROFILE_INVALID',
1203
- );
1204
- validateRegistryFetchProfile(registryProfile);
1205
- equal(
1206
- catalog.registryFetchProfileDigest,
1207
- registryProfile.profileDigest,
1208
- 'LOCK_REGISTRY_PROFILE_INVALID',
1209
- );
1210
- const catalogByKey = new Map(
1211
- catalogEntries.map((entry) => [
1212
- `${asString(entry.package, 'LOCK_CATALOG_INVALID')}@${asString(entry.version, 'LOCK_CATALOG_INVALID')}`,
1213
- entry,
1214
- ]),
1215
- );
1216
- const packageArtifacts = asArray(
1217
- context.packageArtifacts,
1218
- 'LOCK_PACKAGE_BYTES_INVALID',
1219
- ).map((row) =>
1220
- ledger(
1221
- row,
1222
- ['package', 'version', 'bytes', 'runtimeMetadata'],
1223
- 'LOCK_PACKAGE_BYTES_INVALID',
1224
- ),
1225
- );
1226
- const artifactsByKey = new Map();
1227
- for (const artifact of packageArtifacts) {
1228
- const key = `${asString(artifact.package, 'LOCK_PACKAGE_BYTES_INVALID')}@${semver(artifact.version, 'LOCK_PACKAGE_BYTES_INVALID')}`;
1229
- if (artifactsByKey.has(key)) fail('LOCK_PACKAGE_BYTES_INVALID');
1230
- artifactsByKey.set(key, artifact);
1231
- }
1232
- if (artifactsByKey.size !== identities.size)
1233
- fail('LOCK_PACKAGE_BYTES_INVALID');
1234
- for (const [key, row] of identities) {
1235
- const identity = packageIdentity(row, 'LOCK_CATALOG_INVALID');
1236
- const entry = catalogByKey.get(key);
1237
- const artifact = artifactsByKey.get(key);
1238
- if (!entry || !artifact) fail('LOCK_CATALOG_INVALID');
1239
- equal(identity.registry, catalog.registry, 'LOCK_CATALOG_INVALID');
1240
- equal(identity.integrity, entry.integrity, 'LOCK_CATALOG_INVALID');
1241
- const tarball = bytes(artifact.bytes, 'LOCK_PACKAGE_BYTES_INVALID');
1242
- equal(
1243
- identity.integrity,
1244
- sha256Tagged(tarball),
1245
- 'LOCK_PACKAGE_BYTES_INVALID',
1246
- );
1247
- equal(
1248
- BigInt(tarball.length),
1249
- positiveInt64(entry.tarballByteCount, 'LOCK_PACKAGE_BYTES_INVALID'),
1250
- 'LOCK_PACKAGE_BYTES_INVALID',
1251
- );
1252
- const metadata = asObject(
1253
- artifact.runtimeMetadata,
1254
- 'LOCK_RUNTIME_METADATA_INVALID',
1255
- );
1256
- equal(metadata.name, identity.package, 'LOCK_RUNTIME_METADATA_INVALID');
1257
- equal(metadata.version, identity.version, 'LOCK_RUNTIME_METADATA_INVALID');
1258
- equal(
1259
- metadata.license,
1260
- entry.licenseExpression,
1261
- 'LOCK_RUNTIME_METADATA_INVALID',
1262
- );
1263
- }
1264
-
1265
- const derivedEdges = [];
1266
- for (const [key, artifact] of artifactsByKey) {
1267
- const metadata = asObject(
1268
- artifact.runtimeMetadata,
1269
- 'LOCK_RUNTIME_METADATA_INVALID',
1270
- );
1271
- const edges = validateRuntimeMetadata(metadata, identities);
1272
- if (`${metadata.name}@${metadata.version}` !== key)
1273
- fail('LOCK_RUNTIME_METADATA_INVALID');
1274
- derivedEdges.push(...edges);
1275
- }
1276
- const sortedEdges = [...derivedEdges].sort(compareJcs);
1277
- assertCanonicalSet(sortedEdges, 'LOCK_DAG_INVALID');
1278
- const dag = asArray(lock.dependencyDag, 'LOCK_DAG_INVALID').map((edge) =>
1279
- ledger(edge, ['from', 'to'], 'LOCK_DAG_INVALID'),
1280
- );
1281
- if (dag.length > 512) fail('LOCK_DAG_INVALID');
1282
- assertCanonicalSet(dag, 'LOCK_DAG_INVALID');
1283
- jcsEqual(dag, sortedEdges, 'LOCK_DAG_INVALID');
1284
- const adjacency = new Map(
1285
- [...identities.keys()].map((key) => [key, /** @type {string[]} */ ([])]),
1286
- );
1287
- for (const edge of sortedEdges) {
1288
- const from = asString(edge.from, 'LOCK_DAG_INVALID');
1289
- const to = asString(edge.to, 'LOCK_DAG_INVALID');
1290
- const targets = adjacency.get(from);
1291
- if (!targets || !adjacency.has(to)) fail('LOCK_DAG_INVALID');
1292
- targets.push(to);
1293
- }
1294
- const directKeys = new Set(
1295
- direct.map((row) => packageKey(packageIdentity(row, 'LOCK_DAG_INVALID'))),
1296
- );
1297
- const reachable = new Set(directKeys);
1298
- const queue = [...directKeys];
1299
- while (queue.length > 0) {
1300
- const current = queue.shift();
1301
- if (!current) continue;
1302
- for (const target of adjacency.get(current) ?? []) {
1303
- if (!reachable.has(target)) {
1304
- reachable.add(target);
1305
- queue.push(target);
1306
- }
1307
- }
1308
- }
1309
- if (
1310
- dependencies.some(
1311
- (row) =>
1312
- !reachable.has(packageKey(packageIdentity(row, 'LOCK_DAG_INVALID'))),
1313
- )
1314
- ) {
1315
- fail('LOCK_DAG_INVALID');
1316
- }
1317
- const visiting = new Set();
1318
- const visited = new Set();
1319
- /** @param {string} node graph node @returns {void} */
1320
- const visit = (node) => {
1321
- if (visiting.has(node)) fail('LOCK_DAG_INVALID');
1322
- if (visited.has(node)) return;
1323
- visiting.add(node);
1324
- for (const target of adjacency.get(node) ?? []) visit(target);
1325
- visiting.delete(node);
1326
- visited.add(node);
1327
- };
1328
- for (const key of adjacency.keys()) visit(key);
1329
- equal(
1330
- lock.lockDigest,
1331
- digestProfile('lock', lock, 'LOCK_DIGEST_INVALID'),
1332
- 'LOCK_DIGEST_INVALID',
1333
- );
1334
-
1335
- const packages = ledger(
1336
- buildInput.packages,
1337
- ['schemas', 'template', 'theme', 'publisher', 'dependencies'],
1338
- 'BUILD_PACKAGE_MISMATCH',
1339
- );
1340
- const expectedBuildPackages = {
1341
- schemas: packageIdentity(schemas, 'BUILD_PACKAGE_MISMATCH'),
1342
- template: packageIdentity(template, 'BUILD_PACKAGE_MISMATCH'),
1343
- theme: packageIdentity(theme, 'BUILD_PACKAGE_MISMATCH'),
1344
- publisher: publishers.map((row) =>
1345
- packageIdentity(row, 'BUILD_PACKAGE_MISMATCH'),
1346
- ),
1347
- dependencies: dependencies.map((row) =>
1348
- packageIdentity(row, 'BUILD_PACKAGE_MISMATCH'),
1349
- ),
1350
- };
1351
- jcsEqual(packages, expectedBuildPackages, 'BUILD_PACKAGE_MISMATCH');
1352
-
1353
- return {
1354
- direct,
1355
- all,
1356
- identities,
1357
- catalogByKey,
1358
- artifactsByKey,
1359
- catalog,
1360
- adapterPackage,
1361
- };
1362
- }
1363
-
1364
- /**
1365
- * Validate the exact theme/adapter contract projections.
1366
- *
1367
- * @param {Record<string, unknown>} lock lock record
1368
- * @param {Record<string, unknown>} buildInput build input
1369
- * @param {Record<string, unknown>} context trusted context
1370
- * @param {LockState} lockState package state
1371
- * @returns {void}
1372
- */
1373
- function validateContractProjections(lock, buildInput, context, lockState) {
1374
- const code = 'BUILD_CONTRACT_PROJECTION_INVALID';
1375
- const theme = asObject(lock.theme, code);
1376
- const template = asObject(lock.template, code);
1377
- const publishers = asArray(lock.publisher, code).map((row) =>
1378
- asObject(row, code),
1379
- );
1380
- const protocol = publishers[2];
1381
- const adapterPackage = lockState.adapterPackage;
1382
- if (!protocol) fail(code);
1383
- const buildPackages = ledger(
1384
- buildInput.packages,
1385
- ['schemas', 'template', 'theme', 'publisher', 'dependencies'],
1386
- code,
1387
- );
1388
- const expectedBuildPackages = {
1389
- schemas: packageIdentity(asObject(lock.schemas, code), code),
1390
- template: packageIdentity(template, code),
1391
- theme: packageIdentity(theme, code),
1392
- publisher: publishers.map((row) => packageIdentity(row, code)),
1393
- dependencies: asArray(lock.dependencies, code).map((row) =>
1394
- packageIdentity(asObject(row, code), code),
1395
- ),
1396
- };
1397
- jcsEqual(buildPackages, expectedBuildPackages, code);
1398
- equal(buildInput.contractVersion, '2.0.0', code);
1399
- equal(template.contractVersion, '2.0.0', code);
1400
- equal(theme.contractVersion, '2.0.0', code);
1401
-
1402
- const themeContract = asObject(context.themeContract, code);
1403
- const themePackage = asString(theme.package, code);
1404
- const expectedThemeId = THEME_TO_ID[themePackage];
1405
- if (!expectedThemeId) fail(code);
1406
- equal(themeContract.themeId, expectedThemeId, code);
1407
- equal(themeContract.package, packageKey(packageIdentity(theme, code)), code);
1408
- equal(themeContract.contractVersion, theme.contractVersion, code);
1409
- equal(themeContract.templateRange, theme.compatibleWith, code);
1410
- const themeRange = semverRange(themeContract.templateRange, code);
1411
- if (!satisfiesSemverRange(asString(template.version, code), themeRange))
1412
- fail(code);
1413
- const appearance = asObject(buildInput.appearance, code);
1414
- equal(appearance.theme, packageKey(packageIdentity(theme, code)), code);
1415
-
1416
- const adapterCapability = asObject(context.adapterCapability, code);
1417
- const adapter = asObject(adapterCapability.adapter, code);
1418
- const adapterPackageName = asString(adapterPackage.package, code);
1419
- const expectedAdapterId = ADAPTER_TO_ID[adapterPackageName];
1420
- if (!expectedAdapterId) fail(code);
1421
- equal(adapter.adapterId, expectedAdapterId, code);
1422
- equal(adapter.adapterVersion, adapterPackage.version, code);
1423
- equal(adapter.adapterDigest, adapterPackage.integrity, code);
1424
- equal(
1425
- adapterCapability.contractVersion,
1426
- adapterPackage.contractVersion,
1427
- code,
1428
- );
1429
- equal(adapterCapability.protocolRange, adapterPackage.compatibleWith, code);
1430
- const protocolRange = semverRange(adapterCapability.protocolRange, code);
1431
- if (!satisfiesSemverRange(asString(protocol.version, code), protocolRange))
1432
- fail(code);
1433
- equal(
1434
- adapterCapability.capabilityDigest,
1435
- digestProfile('adapterCapability', adapterCapability, code),
1436
- code,
1437
- );
1438
- const destination = ledger(
1439
- buildInput.destinationCapabilities,
1440
- ['adapter', 'baseUrl', 'capabilityDigest'],
1441
- code,
1442
- );
1443
- jcsEqual(destination.adapter, adapter, code);
1444
- equal(destination.capabilityDigest, adapterCapability.capabilityDigest, code);
1445
- }
1446
-
1447
- /**
1448
- * @typedef {{
1449
- * repositoryContext: Record<string, unknown>,
1450
- * treeFiles: Map<string, {path: string, mode: string, objectId: string, bytes: Buffer}>,
1451
- * includedSources: Record<string, unknown>[],
1452
- * exclusions: Record<string, unknown>[],
1453
- * renderPolicy: Record<string, unknown>
1454
- * }} SourceState
1455
- */
1456
-
1457
- /**
1458
- * @param {unknown} value context tree entry
1459
- * @returns {{path: string, mode: string, objectId: string, bytes: Buffer}} validated entry
1460
- */
1461
- function validateTreeEntry(value) {
1462
- const code = 'BUILD_SOURCE_TREE_INVALID';
1463
- const entry = ledger(value, ['path', 'mode', 'objectId', 'bytes'], code);
1464
- const path = asString(entry.path, code);
1465
- validatePortablePath(path);
1466
- const mode = asString(entry.mode, code);
1467
- if (mode !== '100644' && mode !== '100755') fail(code);
1468
- const objectId = gitObjectId(entry.objectId, code);
1469
- const content = bytes(entry.bytes, code);
1470
- if (
1471
- content.length > MAX_SOURCE_FILE_BYTES ||
1472
- !gitBlobMatches(objectId, content)
1473
- ) {
1474
- fail(code);
1475
- }
1476
- return { path, mode, objectId, bytes: content };
1477
- }
1478
-
1479
- /**
1480
- * @param {Record<string, unknown>} body renderable body owner
1481
- * @param {Record<string, unknown>} renderPolicy global render policy
1482
- * @param {Map<string, {path: string, mode: string, objectId: string, bytes: Buffer}>} treeFiles source tree
1483
- * @param {(path: string, digestValue: string, role: string) => void} addSource source closure sink
1484
- * @returns {void}
1485
- */
1486
- function validateRenderableBody(body, renderPolicy, treeFiles, addSource) {
1487
- const code = 'BUILD_RENDER_POLICY_INVALID';
1488
- const sourcePath = asString(body.sourcePath, code);
1489
- const sourceDigest = digest(body.sourceDigest, code);
1490
- addSource(sourcePath, sourceDigest, 'content');
1491
- equal(body.bodyMediaType, 'text/html', code);
1492
- const rendered = asString(body.body, code);
1493
- equal(body.bodyDigest, sha256Tagged(Buffer.from(rendered, 'utf8')), code);
1494
- jcsEqual(body.renderPolicy, renderPolicy, code);
1495
- if (!treeFiles.has(sourcePath)) fail(code);
1496
- }
1497
-
1498
- /**
1499
- * @param {unknown} candidate resolved-file value
1500
- * @param {(path: string, digestValue: string, role: string) => void} addSource source closure sink
1501
- * @returns {void}
1502
- */
1503
- function validateResolvedFile(candidate, addSource) {
1504
- const file = ledger(
1505
- candidate,
1506
- ['path', 'sourceDigest'],
1507
- 'BUILD_SOURCE_REFERENCE_INVALID',
1508
- );
1509
- addSource(
1510
- asString(file.path, 'BUILD_SOURCE_REFERENCE_INVALID'),
1511
- digest(file.sourceDigest, 'BUILD_SOURCE_REFERENCE_INVALID'),
1512
- 'asset',
1513
- );
1514
- }
1515
-
1516
- /**
1517
- * Require a canonical string set.
1518
- *
1519
- * @param {unknown} value candidate array
1520
- * @param {string} code diagnostic
1521
- * @returns {string[]} strings
1522
- */
1523
- function canonicalStringSet(value, code) {
1524
- const values = asArray(value, code).map((entry) => asString(entry, code));
1525
- assertCanonicalSet(values, code, (left, right) =>
1526
- compareUtf8(asString(left, code), asString(right, code)),
1527
- );
1528
- return values;
1529
- }
1530
-
1531
- /**
1532
- * Validate repository/source normalization, lifecycle, author closure, and build digest.
1533
- *
1534
- * @param {Record<string, unknown>} lock lock record
1535
- * @param {Record<string, unknown>} buildInput build-input record
1536
- * @param {Record<string, unknown>} context trusted context
1537
- * @returns {SourceState} validated source state
1538
- */
1539
- function validateBuildInput(lock, buildInput, context) {
1540
- const code = 'BUILD_INPUT_INVALID';
1541
- ledger(buildInput, BUILD_INPUT_KEYS, code);
1542
- equal(buildInput.schemaId, 'urn:gala:schema:build-input:2.0.0', code);
1543
- equal(buildInput.schemaVersion, '2.0.0', code);
1544
- equal(buildInput.contractVersion, '2.0.0', code);
1545
- jcsEqual(buildInput.modules, {}, code);
1546
- jcsEqual(buildInput.placements, [], code);
1547
- timestamp(buildInput.buildEpoch, code);
1548
-
1549
- const repositoryContext = ledger(
1550
- context.repository,
1551
- [
1552
- 'coordinate',
1553
- 'repositoryId',
1554
- 'repositoryOwnerId',
1555
- 'sourceRevision',
1556
- 'sourceTree',
1557
- 'buildEpoch',
1558
- 'treeEntries',
1559
- 'routeNormalizationProfile',
1560
- 'contentRoutes',
1561
- 'exclusions',
1562
- ],
1563
- 'BUILD_SOURCE_TREE_INVALID',
1564
- );
1565
- githubRepositoryCoordinate(
1566
- repositoryContext.coordinate,
1567
- 'BUILD_SOURCE_TREE_INVALID',
1568
- );
1569
- githubPositiveDecimal(
1570
- repositoryContext.repositoryId,
1571
- 'BUILD_SOURCE_TREE_INVALID',
1572
- );
1573
- githubPositiveDecimal(
1574
- repositoryContext.repositoryOwnerId,
1575
- 'BUILD_SOURCE_TREE_INVALID',
1576
- );
1577
- gitObjectId(repositoryContext.sourceRevision, 'BUILD_SOURCE_TREE_INVALID');
1578
- const snapshot = ledger(
1579
- buildInput.repository,
1580
- ['repositoryId', 'repositoryOwnerId', 'sourceRevision', 'rootDigest'],
1581
- 'BUILD_SOURCE_TREE_INVALID',
1582
- );
1583
- equal(
1584
- snapshot.repositoryId,
1585
- repositoryContext.repositoryId,
1586
- 'BUILD_SOURCE_TREE_INVALID',
1587
- );
1588
- equal(
1589
- snapshot.repositoryOwnerId,
1590
- repositoryContext.repositoryOwnerId,
1591
- 'BUILD_SOURCE_TREE_INVALID',
1592
- );
1593
- equal(
1594
- snapshot.sourceRevision,
1595
- repositoryContext.sourceRevision,
1596
- 'BUILD_SOURCE_TREE_INVALID',
1597
- );
1598
- equal(
1599
- buildInput.sourceRevision,
1600
- snapshot.sourceRevision,
1601
- 'BUILD_SOURCE_TREE_INVALID',
1602
- );
1603
- equal(
1604
- buildInput.buildEpoch,
1605
- repositoryContext.buildEpoch,
1606
- 'BUILD_SOURCE_TREE_INVALID',
1607
- );
1608
- gitObjectId(repositoryContext.sourceTree, 'BUILD_SOURCE_TREE_INVALID');
1609
- const treeEntries = asArray(
1610
- repositoryContext.treeEntries,
1611
- 'BUILD_SOURCE_TREE_INVALID',
1612
- ).map(validateTreeEntry);
1613
- const treeFiles = new Map();
1614
- const collisionKeys = new Set();
1615
- let totalSourceBytes = 0n;
1616
- for (const entry of treeEntries) {
1617
- const collisionKey = unicodeCollisionKey17(entry.path);
1618
- if (treeFiles.has(entry.path) || collisionKeys.has(collisionKey)) {
1619
- fail('BUILD_SOURCE_TREE_INVALID');
1620
- }
1621
- treeFiles.set(entry.path, entry);
1622
- collisionKeys.add(collisionKey);
1623
- totalSourceBytes += BigInt(entry.bytes.length);
1624
- }
1625
- if (totalSourceBytes > MAX_SOURCE_BYTES) fail('BUILD_SOURCE_TREE_INVALID');
1626
- const rootEntries = treeEntries
1627
- .map(({ path, mode, objectId }) => ({ path, mode, objectId }))
1628
- .sort((left, right) => compareUtf8(left.path, right.path));
1629
- equal(
1630
- snapshot.rootDigest,
1631
- digestProfile('repositoryRoot', rootEntries, 'BUILD_SOURCE_TREE_INVALID'),
1632
- 'BUILD_SOURCE_TREE_INVALID',
1633
- );
1634
-
1635
- const policyBytes = bytes(
1636
- context.renderPolicyBytes,
1637
- 'BUILD_RENDER_POLICY_INVALID',
1638
- );
1639
- let policyFile;
1640
- try {
1641
- policyFile = asObject(
1642
- parseDuplicateFreeIJson(policyBytes),
1643
- 'BUILD_RENDER_POLICY_INVALID',
1644
- );
1645
- if (!Buffer.from(policyBytes).equals(canonicalizeJcsBytes(policyFile))) {
1646
- fail('BUILD_RENDER_POLICY_INVALID');
1647
- }
1648
- } catch (error) {
1649
- if (error instanceof BuildArtifactSemanticError) throw error;
1650
- fail('BUILD_RENDER_POLICY_INVALID');
1651
- }
1652
- equal(policyFile.name, 'gala-render-policy', 'BUILD_RENDER_POLICY_INVALID');
1653
- equal(
1654
- policyFile.version,
1655
- asObject(lock.template, code).contractVersion,
1656
- 'BUILD_RENDER_POLICY_INVALID',
1657
- );
1658
- if (Object.hasOwn(policyFile, 'digest')) fail('BUILD_RENDER_POLICY_INVALID');
1659
- const renderPolicy = {
1660
- name: 'gala-render-policy',
1661
- version: asString(policyFile.version, 'BUILD_RENDER_POLICY_INVALID'),
1662
- digest: digestRenderPolicyBytes(policyBytes),
1663
- };
1664
-
1665
- /** @type {Map<string, {digest: string, role: string}>} */
1666
- const sourceClosure = new Map();
1667
- /**
1668
- * @param {string} path source path
1669
- * @param {string} digestValue source digest
1670
- * @param {string} role semantic role
1671
- * @returns {void}
1672
- */
1673
- const addSource = (path, digestValue, role) => {
1674
- validatePortablePath(path);
1675
- const file = treeFiles.get(path);
1676
- if (!file || sha256Tagged(file.bytes) !== digestValue) {
1677
- fail('BUILD_SOURCE_REFERENCE_INVALID');
1678
- }
1679
- const prior = sourceClosure.get(path);
1680
- if (prior && prior.digest !== digestValue)
1681
- fail('BUILD_SOURCE_REFERENCE_INVALID');
1682
- if (!SOURCE_ROLES.includes(role)) fail('BUILD_SOURCE_REFERENCE_INVALID');
1683
- if (
1684
- !prior ||
1685
- SOURCE_ROLES.indexOf(role) < SOURCE_ROLES.indexOf(prior.role)
1686
- ) {
1687
- sourceClosure.set(path, { digest: digestValue, role });
1688
- }
1689
- };
1690
-
1691
- const publication = asObject(buildInput.publication, code);
1692
- addSource(
1693
- asString(publication.sourcePath, 'BUILD_SOURCE_REFERENCE_INVALID'),
1694
- digest(publication.sourceDigest, 'BUILD_SOURCE_REFERENCE_INVALID'),
1695
- 'publication',
1696
- );
1697
- if (Object.hasOwn(publication, 'defaultImage')) {
1698
- validateResolvedFile(publication.defaultImage, addSource);
1699
- }
1700
- if (Object.hasOwn(publication, 'profile')) {
1701
- const profile = asObject(publication.profile, code);
1702
- validateRenderableBody(
1703
- asObject(profile.body, code),
1704
- renderPolicy,
1705
- treeFiles,
1706
- addSource,
1707
- );
1708
- }
1709
- if (Object.hasOwn(publication, 'footerCard')) {
1710
- const footer = asObject(publication.footerCard, code);
1711
- validateRenderableBody(
1712
- asObject(footer.body, code),
1713
- renderPolicy,
1714
- treeFiles,
1715
- addSource,
1716
- );
1717
- }
1718
-
1719
- const authors = asArray(buildInput.authors, 'BUILD_AUTHOR_SET_INVALID').map(
1720
- (row) => asObject(row, 'BUILD_AUTHOR_SET_INVALID'),
1721
- );
1722
- const authorIds = new Set();
1723
- let priorAuthorId;
1724
- for (const author of authors) {
1725
- const id = asString(author.id, 'BUILD_AUTHOR_SET_INVALID');
1726
- if (
1727
- authorIds.has(id) ||
1728
- (priorAuthorId !== undefined && compareUtf8(priorAuthorId, id) >= 0)
1729
- ) {
1730
- fail('BUILD_AUTHOR_SET_INVALID');
1731
- }
1732
- priorAuthorId = id;
1733
- authorIds.add(id);
1734
- addSource(
1735
- asString(author.sourcePath, 'BUILD_SOURCE_REFERENCE_INVALID'),
1736
- digest(author.sourceDigest, 'BUILD_SOURCE_REFERENCE_INVALID'),
1737
- 'author',
1738
- );
1739
- if (Object.hasOwn(author, 'avatar'))
1740
- validateResolvedFile(author.avatar, addSource);
1741
- }
1742
-
1743
- const content = asArray(buildInput.content, 'BUILD_LIFECYCLE_INVALID').map(
1744
- (row) => asObject(row, 'BUILD_LIFECYCLE_INVALID'),
1745
- );
1746
- const referencedAuthors = new Set();
1747
- for (const id of asArray(publication.authorIds, 'BUILD_AUTHOR_SET_INVALID')) {
1748
- referencedAuthors.add(asString(id, 'BUILD_AUTHOR_SET_INVALID'));
1749
- }
1750
- for (const optional of ['contactAuthorId']) {
1751
- if (Object.hasOwn(publication, optional)) {
1752
- referencedAuthors.add(
1753
- asString(publication[optional], 'BUILD_AUTHOR_SET_INVALID'),
1754
- );
1755
- }
1756
- }
1757
- if (Object.hasOwn(publication, 'footerCard')) {
1758
- const footer = asObject(publication.footerCard, 'BUILD_AUTHOR_SET_INVALID');
1759
- for (const id of asArray(footer.authorIds, 'BUILD_AUTHOR_SET_INVALID')) {
1760
- referencedAuthors.add(asString(id, 'BUILD_AUTHOR_SET_INVALID'));
1761
- }
1762
- }
1763
- let previousContentKey;
1764
- const contentIds = new Set();
1765
- for (const record of content) {
1766
- const frontmatter = asObject(record.frontmatter, 'BUILD_LIFECYCLE_INVALID');
1767
- const id = asString(frontmatter.id, 'BUILD_LIFECYCLE_INVALID');
1768
- const sourcePath = asString(
1769
- record.sourcePath,
1770
- 'BUILD_SOURCE_REFERENCE_INVALID',
1771
- );
1772
- const key = `${id}\0${sourcePath}`;
1773
- if (
1774
- contentIds.has(id) ||
1775
- (previousContentKey !== undefined &&
1776
- compareUtf8(previousContentKey, key) >= 0)
1777
- ) {
1778
- fail('BUILD_LIFECYCLE_INVALID');
1779
- }
1780
- previousContentKey = key;
1781
- contentIds.add(id);
1782
- const status = asString(frontmatter.status, 'BUILD_LIFECYCLE_INVALID');
1783
- if (status !== 'published' && status !== 'unlisted')
1784
- fail('BUILD_LIFECYCLE_INVALID');
1785
- if (!Object.hasOwn(frontmatter, 'publishedAt'))
1786
- fail('BUILD_LIFECYCLE_INVALID');
1787
- const createdAt = timestamp(
1788
- frontmatter.createdAt,
1789
- 'BUILD_LIFECYCLE_INVALID',
1790
- );
1791
- const publishedAt = timestamp(
1792
- frontmatter.publishedAt,
1793
- 'BUILD_LIFECYCLE_INVALID',
1794
- );
1795
- if (publishedAt < createdAt) fail('BUILD_LIFECYCLE_INVALID');
1796
- if (Object.hasOwn(frontmatter, 'updatedAt')) {
1797
- const updatedAt = timestamp(
1798
- frontmatter.updatedAt,
1799
- 'BUILD_LIFECYCLE_INVALID',
1800
- );
1801
- if (updatedAt < createdAt || updatedAt < publishedAt)
1802
- fail('BUILD_LIFECYCLE_INVALID');
1803
- }
1804
- if (
1805
- Object.hasOwn(frontmatter, 'seriesOrder') &&
1806
- !Object.hasOwn(frontmatter, 'series')
1807
- ) {
1808
- fail('BUILD_LIFECYCLE_INVALID');
1809
- }
1810
- canonicalStringSet(frontmatter.tags, 'BUILD_LIFECYCLE_INVALID');
1811
- canonicalStringSet(frontmatter.redirects, 'BUILD_LIFECYCLE_INVALID');
1812
- const frontmatterAuthors = asArray(
1813
- frontmatter.authorIds,
1814
- 'BUILD_AUTHOR_SET_INVALID',
1815
- ).map((authorId) => asString(authorId, 'BUILD_AUTHOR_SET_INVALID'));
1816
- jcsEqual(
1817
- record.resolvedAuthorIds,
1818
- frontmatterAuthors,
1819
- 'BUILD_AUTHOR_SET_INVALID',
1820
- );
1821
- for (const authorId of frontmatterAuthors) referencedAuthors.add(authorId);
1822
- equal(
1823
- record.sourceRevision,
1824
- buildInput.sourceRevision,
1825
- 'BUILD_SOURCE_TREE_INVALID',
1826
- );
1827
- const sourceDigest = digest(
1828
- record.sourceDigest,
1829
- 'BUILD_SOURCE_REFERENCE_INVALID',
1830
- );
1831
- addSource(sourcePath, sourceDigest, 'content');
1832
- validateRenderableBody(record, renderPolicy, treeFiles, addSource);
1833
- if (Object.hasOwn(frontmatter, 'hero')) {
1834
- const hero = asObject(frontmatter.hero, 'BUILD_SOURCE_REFERENCE_INVALID');
1835
- validateResolvedFile(hero.file, addSource);
1836
- if (
1837
- (hero.role === 'decorative' && hero.alt !== '') ||
1838
- (hero.role === 'informative' &&
1839
- asString(hero.alt, 'BUILD_SOURCE_REFERENCE_INVALID').length === 0)
1840
- ) {
1841
- fail('BUILD_SOURCE_REFERENCE_INVALID');
1842
- }
1843
- }
1844
- if (Object.hasOwn(frontmatter, 'socialImage')) {
1845
- validateResolvedFile(frontmatter.socialImage, addSource);
1846
- }
1847
- }
1848
- if (
1849
- referencedAuthors.size !== authorIds.size ||
1850
- [...referencedAuthors].some((id) => !authorIds.has(id))
1851
- ) {
1852
- fail('BUILD_AUTHOR_SET_INVALID');
1853
- }
1854
-
1855
- const navigation = asObject(buildInput.navigation, code);
1856
- const navigationSource = asObject(navigation.source, code);
1857
- if (navigationSource.kind === 'authored') {
1858
- addSource(
1859
- asString(navigationSource.sourcePath, 'BUILD_SOURCE_REFERENCE_INVALID'),
1860
- digest(navigationSource.sourceDigest, 'BUILD_SOURCE_REFERENCE_INVALID'),
1861
- 'navigation',
1862
- );
1863
- } else {
1864
- const expectedNavigation = {
1865
- items: [],
1866
- footerItems: [],
1867
- source: {
1868
- kind: 'built-in-default',
1869
- defaultId: 'urn:gala:normalized-default:navigation:2.0.0',
1870
- defaultDigest: '',
1871
- },
1872
- };
1873
- expectedNavigation.source.defaultDigest = digestProfile(
1874
- 'navigationDefault',
1875
- expectedNavigation,
1876
- code,
1877
- );
1878
- jcsEqual(navigation, expectedNavigation, code);
1879
- }
1880
- const appearance = asObject(buildInput.appearance, code);
1881
- const appearanceSource = asObject(appearance.source, code);
1882
- const colorMode = asObject(appearance.colorMode, code);
1883
- const allowedColorModes = canonicalStringSet(colorMode.allowed, code);
1884
- if (
1885
- !allowedColorModes.includes(asString(colorMode.default, code)) ||
1886
- allowedColorModes.some(
1887
- (mode) => !['dark', 'light', 'system'].includes(mode),
1888
- )
1889
- ) {
1890
- fail(code);
1891
- }
1892
- for (const key of ['brandMark', 'wordmark']) {
1893
- if (Object.hasOwn(appearance, key))
1894
- validateResolvedFile(appearance[key], addSource);
1895
- }
1896
- for (const file of asArray(
1897
- appearance.fontAssets,
1898
- 'BUILD_SOURCE_REFERENCE_INVALID',
1899
- )) {
1900
- validateResolvedFile(file, addSource);
1901
- }
1902
- if (appearanceSource.kind === 'authored') {
1903
- addSource(
1904
- asString(appearanceSource.sourcePath, 'BUILD_SOURCE_REFERENCE_INVALID'),
1905
- digest(appearanceSource.sourceDigest, 'BUILD_SOURCE_REFERENCE_INVALID'),
1906
- 'appearance',
1907
- );
1908
- } else {
1909
- const expectedAppearance = {
1910
- theme: '@rathnasgala2/theme-default@2.0.0',
1911
- colorMode: { allowed: ['dark', 'light', 'system'], default: 'system' },
1912
- headerComposition: 'name-only',
1913
- footerComposition: 'compact',
1914
- typeScale: 'standard',
1915
- fontAssets: [],
1916
- tokens: {},
1917
- source: {
1918
- kind: 'built-in-default',
1919
- defaultId: 'urn:gala:normalized-default:appearance:2.0.0',
1920
- defaultDigest: '',
1921
- },
1922
- };
1923
- expectedAppearance.source.defaultDigest = digestProfile(
1924
- 'appearanceDefault',
1925
- expectedAppearance,
1926
- code,
1927
- );
1928
- jcsEqual(appearance, expectedAppearance, code);
1929
- }
1930
-
1931
- const normalizedSource = ledger(
1932
- context.normalizedSource,
1933
- ['publication', 'authors', 'content', 'navigation', 'appearance'],
1934
- 'BUILD_SOURCE_PROJECTION_INVALID',
1935
- );
1936
- for (const key of [
1937
- 'publication',
1938
- 'authors',
1939
- 'content',
1940
- 'navigation',
1941
- 'appearance',
1942
- ]) {
1943
- jcsEqual(
1944
- buildInput[key],
1945
- normalizedSource[key],
1946
- 'BUILD_SOURCE_PROJECTION_INVALID',
1947
- );
1948
- }
1949
-
1950
- const baseUrl = asString(buildInput.baseUrl, code);
1951
- let parsedBase;
1952
- try {
1953
- parsedBase = new URL(baseUrl);
1954
- } catch {
1955
- fail(code);
1956
- }
1957
- if (
1958
- parsedBase.protocol !== 'https:' ||
1959
- parsedBase.username !== '' ||
1960
- parsedBase.password !== '' ||
1961
- parsedBase.origin !== baseUrl ||
1962
- parsedBase.pathname !== '/' ||
1963
- parsedBase.search !== '' ||
1964
- parsedBase.hash !== ''
1965
- ) {
1966
- fail(code);
1967
- }
1968
- const basePath = asString(buildInput.basePath, code);
1969
- if (
1970
- !basePath.startsWith('/') ||
1971
- !basePath.endsWith('/') ||
1972
- basePath.includes('//')
1973
- ) {
1974
- if (basePath !== '/') fail(code);
1975
- }
1976
- const publicRoot = `${baseUrl}${basePath}`;
1977
- equal(publication.canonicalBase, publicRoot, code);
1978
- const destination = asObject(buildInput.destinationCapabilities, code);
1979
- equal(destination.baseUrl, publicRoot, code);
1980
-
1981
- const includedSources = [...sourceClosure.entries()]
1982
- .map(([path, owner]) => ({
1983
- path,
1984
- sha256: owner.digest,
1985
- sourceRevision: asString(buildInput.sourceRevision, code),
1986
- role: owner.role,
1987
- }))
1988
- .sort(compareJcs);
1989
- const exclusions = asArray(
1990
- repositoryContext.exclusions,
1991
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
1992
- ).map((candidate) =>
1993
- ledger(
1994
- candidate,
1995
- ['path', 'ruleId', 'reason'],
1996
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
1997
- ),
1998
- );
1999
- const excludedPaths = new Set();
2000
- for (const exclusion of exclusions) {
2001
- const path = asString(exclusion.path, 'ARTIFACT_SOURCE_PARTITION_INVALID');
2002
- if (
2003
- excludedPaths.has(path) ||
2004
- sourceClosure.has(path) ||
2005
- !treeFiles.has(path)
2006
- ) {
2007
- fail('ARTIFACT_SOURCE_PARTITION_INVALID');
2008
- }
2009
- excludedPaths.add(path);
2010
- }
2011
- if (
2012
- sourceClosure.size + excludedPaths.size !== treeFiles.size ||
2013
- [...treeFiles.keys()].some(
2014
- (path) => !sourceClosure.has(path) && !excludedPaths.has(path),
2015
- )
2016
- ) {
2017
- fail('ARTIFACT_SOURCE_PARTITION_INVALID');
2018
- }
2019
- equal(
2020
- buildInput.inputDigest,
2021
- digestProfile('buildInput', buildInput, 'BUILD_DIGEST_INVALID'),
2022
- 'BUILD_DIGEST_INVALID',
2023
- );
2024
- return {
2025
- repositoryContext,
2026
- treeFiles,
2027
- includedSources,
2028
- exclusions,
2029
- renderPolicy,
2030
- };
2031
- }
2032
-
2033
- /**
2034
- * Decode one canonical unpadded base64url value.
2035
- *
2036
- * @param {unknown} value candidate value
2037
- * @param {string} code diagnostic
2038
- * @returns {Buffer} decoded bytes
2039
- */
2040
- function decodeBase64url(value, code) {
2041
- const source = asString(value, code);
2042
- if (!/^[A-Za-z0-9_-]+$/u.test(source)) fail(code);
2043
- const decoded = Buffer.from(source, 'base64url');
2044
- if (decoded.length === 0 || decoded.toString('base64url') !== source)
2045
- fail(code);
2046
- return decoded;
2047
- }
2048
-
2049
- /**
2050
- * Convert a canonical Gala timestamp to whole seconds since Unix epoch.
2051
- *
2052
- * @param {string} value canonical timestamp
2053
- * @returns {bigint} epoch seconds
2054
- */
2055
- function timestampSeconds(value) {
2056
- validateRfc3339(value);
2057
- const year = BigInt(value.slice(0, 4));
2058
- const month = Number(value.slice(5, 7));
2059
- const day = Number(value.slice(8, 10));
2060
- const hour = BigInt(value.slice(11, 13));
2061
- const minute = BigInt(value.slice(14, 16));
2062
- const second = BigInt(value.slice(17, 19));
2063
- /**
2064
- * @param {bigint} candidateYear proleptic Gregorian year
2065
- * @returns {bigint} days before year
2066
- */
2067
- const daysBeforeYear = (candidateYear) =>
2068
- 365n * candidateYear +
2069
- (candidateYear + 3n) / 4n -
2070
- (candidateYear + 99n) / 100n +
2071
- (candidateYear + 399n) / 400n;
2072
- const leap = year % 4n === 0n && (year % 100n !== 0n || year % 400n === 0n);
2073
- const monthLengths = [
2074
- 31,
2075
- leap ? 29 : 28,
2076
- 31,
2077
- 30,
2078
- 31,
2079
- 30,
2080
- 31,
2081
- 31,
2082
- 30,
2083
- 31,
2084
- 30,
2085
- 31,
2086
- ];
2087
- const priorMonthDays = monthLengths
2088
- .slice(0, month - 1)
2089
- .reduce((total, length) => total + BigInt(length), 0n);
2090
- const days =
2091
- daysBeforeYear(year) -
2092
- daysBeforeYear(1970n) +
2093
- priorMonthDays +
2094
- BigInt(day - 1);
2095
- return days * 86_400n + hour * 3_600n + minute * 60n + second;
2096
- }
2097
-
2098
- /**
2099
- * Parse and validate a projected OIDC key.
2100
- *
2101
- * @param {unknown} value candidate key
2102
- * @returns {Record<string, unknown>} projected key
2103
- */
2104
- function validateOidcKey(value) {
2105
- const code = 'WORKLOAD_KEY_SET_INVALID';
2106
- const source = asObject(value, code);
2107
- const optional = Object.hasOwn(source, 'x5t') ? ['x5t'] : [];
2108
- const key = ledger(
2109
- source,
2110
- ['kty', 'alg', 'use', 'kid', 'n', 'e'],
2111
- code,
2112
- optional,
2113
- );
2114
- equal(key.kty, 'RSA', code);
2115
- equal(key.alg, 'RS256', code);
2116
- equal(key.use, 'sig', code);
2117
- const kid = asString(key.kid, code);
2118
- if (kid.length < 1 || kid.length > 128 || !/^[\x21-\x7e]+$/u.test(kid))
2119
- fail(code);
2120
- const modulus = decodeBase64url(key.n, code);
2121
- if (
2122
- modulus.length < 256 ||
2123
- modulus.length > 512 ||
2124
- modulus[0] === 0 ||
2125
- ((modulus.at(-1) ?? 0) & 1) === 0
2126
- ) {
2127
- fail(code);
2128
- }
2129
- equal(key.e, 'AQAB', code);
2130
- if (
2131
- Object.hasOwn(key, 'x5t') &&
2132
- decodeBase64url(key.x5t, code).length !== 20
2133
- ) {
2134
- fail(code);
2135
- }
2136
- return key;
2137
- }
2138
-
2139
- /**
2140
- * Validate the exact retained OIDC verification profile.
2141
- *
2142
- * @param {unknown} value candidate profile
2143
- * @returns {Record<string, unknown>} validated profile
2144
- */
2145
- function validateOidcProfile(value) {
2146
- const code = 'WORKLOAD_PROFILE_INVALID';
2147
- const profile = ledger(
2148
- value,
2149
- [
2150
- 'profile',
2151
- 'issuer',
2152
- 'discoveryUrl',
2153
- 'jwksUrl',
2154
- 'signingAlgorithm',
2155
- 'maximumKeyCount',
2156
- 'maximumJwksEntityBytes',
2157
- 'maximumJwksWireBodyBytes',
2158
- 'maximumResponseHeadBytes',
2159
- 'requestTimeoutMillis',
2160
- 'cacheFreshSeconds',
2161
- 'maximumClockSkewSeconds',
2162
- 'maximumTokenLifetimeSeconds',
2163
- 'staleKeyUse',
2164
- 'unknownKidRefreshesPerExchange',
2165
- 'redirects',
2166
- 'ambientProxy',
2167
- 'netrc',
2168
- 'cookies',
2169
- 'credentials',
2170
- 'networkBoundaryProfileDigest',
2171
- 'tlsProfileDigest',
2172
- 'discoveryEvidenceDigest',
2173
- 'fixtureDigest',
2174
- 'profileDigest',
2175
- ],
2176
- code,
2177
- );
2178
- const fixed = {
2179
- profile: 'gala-github-workload-oidc-verification-v2',
2180
- issuer: 'https://token.actions.githubusercontent.com',
2181
- discoveryUrl:
2182
- 'https://token.actions.githubusercontent.com/.well-known/openid-configuration',
2183
- jwksUrl: 'https://token.actions.githubusercontent.com/.well-known/jwks',
2184
- signingAlgorithm: 'RS256',
2185
- maximumKeyCount: 32,
2186
- maximumJwksEntityBytes: 262_144,
2187
- maximumJwksWireBodyBytes: 1_572_869,
2188
- maximumResponseHeadBytes: 32_768,
2189
- requestTimeoutMillis: 5_000,
2190
- cacheFreshSeconds: 300,
2191
- maximumClockSkewSeconds: 60,
2192
- maximumTokenLifetimeSeconds: 600,
2193
- staleKeyUse: 'reject',
2194
- unknownKidRefreshesPerExchange: 1,
2195
- redirects: 'reject',
2196
- ambientProxy: 'disabled',
2197
- netrc: 'disabled',
2198
- cookies: 'disabled',
2199
- credentials: [],
2200
- };
2201
- for (const [key, expected] of Object.entries(fixed))
2202
- jcsEqual(profile[key], expected, code);
2203
- for (const key of [
2204
- 'networkBoundaryProfileDigest',
2205
- 'tlsProfileDigest',
2206
- 'discoveryEvidenceDigest',
2207
- 'fixtureDigest',
2208
- 'profileDigest',
2209
- ]) {
2210
- digest(profile[key], code);
2211
- }
2212
- equal(
2213
- profile.profileDigest,
2214
- digestProfile('githubWorkloadOidcVerificationProfile', profile, code),
2215
- code,
2216
- );
2217
- return profile;
2218
- }
2219
-
2220
- /**
2221
- * Validate the authority-bearing GitHub claim spellings and their exact
2222
- * workflow-ledger projections.
2223
- *
2224
- * @param {Record<string, unknown>} claims exact retained claim projection
2225
- * @param {Record<string, unknown>} profile accepted OIDC profile
2226
- * @param {Record<string, unknown>[]} workflowFiles retained workflow evidence
2227
- * @param {Record<string, unknown>} repositoryContext repository facts
2228
- * @returns {{workflowTriggerCommit: string, runAttempt: number}} normalized authority values
2229
- */
2230
- function validateWorkloadClaims(
2231
- claims,
2232
- profile,
2233
- workflowFiles,
2234
- repositoryContext,
2235
- ) {
2236
- const code = 'WORKLOAD_INVALID';
2237
- equal(claims.iss, profile.issuer, code);
2238
- equal(claims.aud, 'urn:gala:workload:deployment-intent:v2', code);
2239
- const repository = githubRepositoryCoordinate(claims.repository, code);
2240
- equal(repository.coordinate, repositoryContext.coordinate, code);
2241
- equal(claims.repository_owner, repository.owner, code);
2242
- equal(
2243
- githubPositiveDecimal(claims.repository_id, code),
2244
- repositoryContext.repositoryId,
2245
- code,
2246
- );
2247
- equal(
2248
- githubPositiveDecimal(claims.repository_owner_id, code),
2249
- repositoryContext.repositoryOwnerId,
2250
- code,
2251
- );
2252
- githubPositiveDecimal(claims.actor_id, code);
2253
- githubPositiveDecimal(claims.run_id, code);
2254
- githubPositiveDecimal(claims.run_number, code);
2255
- const attemptSource = asString(claims.run_attempt, code);
2256
- if (!POSITIVE_DECIMAL_PATTERN.test(attemptSource)) fail(code);
2257
- const runAttempt = Number(attemptSource);
2258
- if (!Number.isInteger(runAttempt) || runAttempt < 1 || runAttempt > 51) {
2259
- fail(code);
2260
- }
2261
- const actor = asString(claims.actor, code);
2262
- if (
2263
- Buffer.byteLength(actor, 'ascii') !== Buffer.byteLength(actor, 'utf8') ||
2264
- Buffer.byteLength(actor, 'ascii') > 100 ||
2265
- !/^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?|[A-Za-z0-9](?:[A-Za-z0-9-]{0,92}[A-Za-z0-9])?\[bot\])$/u.test(
2266
- actor,
2267
- )
2268
- ) {
2269
- fail(code);
2270
- }
2271
- if (
2272
- !['create', 'workflow_dispatch'].includes(asString(claims.event_name, code))
2273
- ) {
2274
- fail(code);
2275
- }
2276
- equal(claims.runner_environment, 'github-hosted', code);
2277
- const ref = asString(claims.ref, code);
2278
- const prefix = 'refs/heads/gala/publish/';
2279
- if (
2280
- Buffer.byteLength(ref, 'ascii') !== Buffer.byteLength(ref, 'utf8') ||
2281
- ref.length > 512 ||
2282
- !ref.startsWith(prefix)
2283
- ) {
2284
- fail(code);
2285
- }
2286
- stableId(ref.slice(prefix.length), code);
2287
- const workflowTriggerCommit = githubSha(claims.sha, code);
2288
- equal(githubSha(claims.workflow_sha, code), workflowTriggerCommit, code);
2289
- const caller = workflowFiles[0];
2290
- const publish = workflowFiles[1];
2291
- const authorize = workflowFiles[2];
2292
- const report = workflowFiles[3];
2293
- if (!caller || !publish || !authorize || !report) fail(code);
2294
- equal(caller.commit, workflowTriggerCommit, code);
2295
- equal(
2296
- claims.workflow_ref,
2297
- `${repository.coordinate}/${asString(caller.path, code)}@${ref}`,
2298
- code,
2299
- );
2300
- const calledCommit = githubSha(claims.job_workflow_sha, code);
2301
- equal(authorize.commit, calledCommit, code);
2302
- equal(
2303
- claims.job_workflow_ref,
2304
- `rathnasgala2/publish/${asString(authorize.path, code)}@${asString(claims.job_workflow_sha, code)}`,
2305
- code,
2306
- );
2307
- equal(publish.repositoryId, authorize.repositoryId, code);
2308
- equal(report.repositoryId, authorize.repositoryId, code);
2309
- equal(publish.commit, authorize.commit, code);
2310
- equal(report.commit, authorize.commit, code);
2311
- const expectedSubjects = [
2312
- `repo:${repository.coordinate}:ref:${ref}`,
2313
- `repo:${repository.owner}@${asString(claims.repository_owner_id, code)}/${repository.repository}@${asString(claims.repository_id, code)}:ref:${ref}`,
2314
- ];
2315
- if (!expectedSubjects.includes(asString(claims.sub, code))) fail(code);
2316
- const jti = asString(claims.jti, code);
2317
- if (jti.length === 0) fail(code);
2318
- return { workflowTriggerCommit, runAttempt };
2319
- }
2320
-
2321
- /**
2322
- * Validate compact JWS, rotating-key evidence, required claims, and binding.
2323
- *
2324
- * @param {Record<string, unknown>} workload workload context
2325
- * @param {Record<string, unknown>[]} workflowFiles exact workflow records
2326
- * @param {SourceState} sourceState source state
2327
- * @returns {{binding: Record<string, unknown>, claims: Record<string, unknown>, workflowTriggerCommit: string}} workload state
2328
- */
2329
- function validateWorkload(workload, workflowFiles, sourceState) {
2330
- const code = 'WORKLOAD_INVALID';
2331
- ledger(
2332
- workload,
2333
- [
2334
- 'compactJwt',
2335
- 'expectedClaims',
2336
- 'verificationProfile',
2337
- 'issuerKeys',
2338
- 'issuerKeySetObservedAt',
2339
- 'trustedNowEpochSeconds',
2340
- 'commitNowEpochSeconds',
2341
- 'authorizedAt',
2342
- 'verifiedBinding',
2343
- ],
2344
- code,
2345
- );
2346
- const profile = validateOidcProfile(workload.verificationProfile);
2347
- const keys = asArray(workload.issuerKeys, 'WORKLOAD_KEY_SET_INVALID').map(
2348
- validateOidcKey,
2349
- );
2350
- if (keys.length < 1 || keys.length > 32) fail('WORKLOAD_KEY_SET_INVALID');
2351
- assertCanonicalSet(keys, 'WORKLOAD_KEY_SET_INVALID', (left, right) =>
2352
- compareUtf8(
2353
- asString(asObject(left, code).kid, code),
2354
- asString(asObject(right, code).kid, code),
2355
- ),
2356
- );
2357
- const semanticKeys = new Set();
2358
- for (const key of keys) {
2359
- const semantic = canonicalizeJcs({
2360
- n: key.n,
2361
- e: key.e,
2362
- x5t: key.x5t ?? null,
2363
- });
2364
- if (semanticKeys.has(semantic)) fail('WORKLOAD_KEY_SET_INVALID');
2365
- semanticKeys.add(semantic);
2366
- }
2367
- const keySetDigest = digestProfile(
2368
- 'githubWorkloadOidcKeySet',
2369
- keys,
2370
- 'WORKLOAD_KEY_SET_INVALID',
2371
- );
2372
- const observedAt = timestamp(workload.issuerKeySetObservedAt, code);
2373
- let now;
2374
- try {
2375
- now = BigInt(asString(workload.trustedNowEpochSeconds, code));
2376
- } catch {
2377
- fail(code);
2378
- }
2379
- const observedSeconds = timestampSeconds(observedAt);
2380
- if (now < observedSeconds || now > observedSeconds + 300n) fail(code);
2381
-
2382
- const compact = asString(workload.compactJwt, code);
2383
- const segments = compact.split('.');
2384
- if (segments.length !== 3) fail(code);
2385
- const headerSegment = segments[0];
2386
- const payloadSegment = segments[1];
2387
- const signatureSegment = segments[2];
2388
- if (!headerSegment || !payloadSegment || !signatureSegment) fail(code);
2389
- let header;
2390
- let claims;
2391
- try {
2392
- header = asObject(
2393
- parseDuplicateFreeIJson(decodeBase64url(headerSegment, code)),
2394
- code,
2395
- );
2396
- claims = asObject(
2397
- parseDuplicateFreeIJson(decodeBase64url(payloadSegment, code)),
2398
- code,
2399
- );
2400
- } catch (error) {
2401
- if (error instanceof BuildArtifactSemanticError) throw error;
2402
- fail(code);
2403
- }
2404
- ledger(header, ['alg', 'kid'], code, ['typ', 'x5t']);
2405
- equal(header.alg, 'RS256', code);
2406
- if (Object.hasOwn(header, 'typ')) equal(header.typ, 'JWT', code);
2407
- const key = keys.find((candidate) => candidate.kid === header.kid);
2408
- if (!key) fail(code);
2409
- if (Object.hasOwn(header, 'x5t')) {
2410
- decodeBase64url(header.x5t, code);
2411
- equal(header.x5t, key.x5t, code);
2412
- }
2413
- const signature = decodeBase64url(signatureSegment, code);
2414
- const modulus = decodeBase64url(key.n, code);
2415
- if (signature.length !== modulus.length) fail(code);
2416
- let signatureValid = false;
2417
- try {
2418
- const publicKey = createPublicKey({
2419
- key: { kty: 'RSA', n: asString(key.n, code), e: 'AQAB' },
2420
- format: 'jwk',
2421
- });
2422
- signatureValid = verifySignature(
2423
- 'RSA-SHA256',
2424
- Buffer.from(`${headerSegment}.${payloadSegment}`, 'ascii'),
2425
- publicKey,
2426
- signature,
2427
- );
2428
- } catch {
2429
- fail(code);
2430
- }
2431
- if (!signatureValid) fail(code);
2432
-
2433
- const expectedClaims = ledger(workload.expectedClaims, OIDC_CLAIMS, code);
2434
- for (const claim of OIDC_CLAIMS) {
2435
- equal(claims[claim], expectedClaims[claim], code);
2436
- asString(claims[claim], code);
2437
- }
2438
- const authority = validateWorkloadClaims(
2439
- expectedClaims,
2440
- profile,
2441
- workflowFiles,
2442
- sourceState.repositoryContext,
2443
- );
2444
- for (const claim of ['nbf', 'iat', 'exp']) {
2445
- if (!Object.hasOwn(claims, claim)) fail(code);
2446
- }
2447
- const nbf = claims.nbf;
2448
- const iat = claims.iat;
2449
- const exp = claims.exp;
2450
- if (
2451
- typeof nbf !== 'number' ||
2452
- typeof iat !== 'number' ||
2453
- typeof exp !== 'number' ||
2454
- !Number.isSafeInteger(nbf) ||
2455
- !Number.isSafeInteger(iat) ||
2456
- !Number.isSafeInteger(exp)
2457
- ) {
2458
- fail(code);
2459
- }
2460
- const nbfSeconds = BigInt(nbf);
2461
- const iatSeconds = BigInt(iat);
2462
- const expSeconds = BigInt(exp);
2463
- if (
2464
- nbfSeconds > iatSeconds ||
2465
- iatSeconds >= expSeconds ||
2466
- iatSeconds < now - 60n ||
2467
- iatSeconds > now + 60n ||
2468
- nbfSeconds > now + 60n ||
2469
- expSeconds <= now ||
2470
- expSeconds - iatSeconds > 600n
2471
- ) {
2472
- fail(code);
2473
- }
2474
- let commitNow;
2475
- try {
2476
- commitNow = BigInt(asString(workload.commitNowEpochSeconds, code));
2477
- } catch {
2478
- fail(code);
2479
- }
2480
- if (commitNow >= expSeconds) fail(code);
2481
-
2482
- const binding = ledger(
2483
- workload.verifiedBinding,
2484
- [
2485
- 'issuer',
2486
- 'audience',
2487
- 'repository',
2488
- 'repositoryId',
2489
- 'repositoryOwner',
2490
- 'repositoryOwnerId',
2491
- 'ref',
2492
- 'sourceCommit',
2493
- 'workflowTriggerCommit',
2494
- 'runId',
2495
- 'runNumber',
2496
- 'runAttempt',
2497
- 'eventName',
2498
- 'actor',
2499
- 'actorId',
2500
- 'callerWorkflow',
2501
- 'publishWorkflow',
2502
- 'authorizeWorkflow',
2503
- 'oidcVerificationProfileDigest',
2504
- 'issuerKeySetDigest',
2505
- 'issuerKeySetObservedAt',
2506
- 'verifiedAt',
2507
- 'workloadBindingDigest',
2508
- ],
2509
- code,
2510
- );
2511
- const repositoryContext = sourceState.repositoryContext;
2512
- const expectedBinding = {
2513
- issuer: expectedClaims.iss,
2514
- audience: expectedClaims.aud,
2515
- repository: expectedClaims.repository,
2516
- repositoryId: expectedClaims.repository_id,
2517
- repositoryOwner: expectedClaims.repository_owner,
2518
- repositoryOwnerId: expectedClaims.repository_owner_id,
2519
- ref: expectedClaims.ref,
2520
- sourceCommit: repositoryContext.sourceRevision,
2521
- workflowTriggerCommit: authority.workflowTriggerCommit,
2522
- runId: expectedClaims.run_id,
2523
- runNumber: expectedClaims.run_number,
2524
- runAttempt: authority.runAttempt,
2525
- eventName: expectedClaims.event_name,
2526
- actor: expectedClaims.actor,
2527
- actorId: expectedClaims.actor_id,
2528
- callerWorkflow: workflowFiles[0],
2529
- publishWorkflow: workflowFiles[1],
2530
- authorizeWorkflow: workflowFiles[2],
2531
- oidcVerificationProfileDigest: profile.profileDigest,
2532
- issuerKeySetDigest: keySetDigest,
2533
- issuerKeySetObservedAt: observedAt,
2534
- verifiedAt: timestamp(workload.authorizedAt, code),
2535
- workloadBindingDigest: '',
2536
- };
2537
- expectedBinding.workloadBindingDigest = digestProfile(
2538
- 'verifiedWorkloadBinding',
2539
- expectedBinding,
2540
- code,
2541
- );
2542
- jcsEqual(binding, expectedBinding, code);
2543
- equal(binding.repository, repositoryContext.coordinate, code);
2544
- equal(binding.repositoryId, repositoryContext.repositoryId, code);
2545
- equal(binding.repositoryOwnerId, repositoryContext.repositoryOwnerId, code);
2546
- return {
2547
- binding,
2548
- claims,
2549
- workflowTriggerCommit: authority.workflowTriggerCommit,
2550
- };
2551
- }
2552
-
2553
- /**
2554
- * Remove a YAML comment without interpreting hashes inside quoted scalars.
2555
- *
2556
- * @param {string} line one physical YAML line
2557
- * @returns {string} code portion of the line
2558
- */
2559
- function yamlCodeLine(line) {
2560
- let singleQuoted = false;
2561
- let doubleQuoted = false;
2562
- let escaped = false;
2563
- for (let index = 0; index < line.length; index += 1) {
2564
- const character = line[index];
2565
- if (doubleQuoted) {
2566
- if (escaped) escaped = false;
2567
- else if (character === '\\') escaped = true;
2568
- else if (character === '"') doubleQuoted = false;
2569
- continue;
2570
- }
2571
- if (singleQuoted) {
2572
- if (character !== "'") continue;
2573
- if (line[index + 1] === "'") index += 1;
2574
- else singleQuoted = false;
2575
- continue;
2576
- }
2577
- if (character === '"') doubleQuoted = true;
2578
- else if (character === "'") singleQuoted = true;
2579
- else if (
2580
- character === '#' &&
2581
- (index === 0 || /\s/u.test(line[index - 1] ?? ''))
2582
- ) {
2583
- return line.slice(0, index);
2584
- }
2585
- }
2586
- return line;
2587
- }
2588
-
2589
- /**
2590
- * Mask quoted YAML scalar content for unsupported-flow-key detection.
2591
- *
2592
- * @param {string} line comment-free YAML line
2593
- * @returns {string} equal-length syntax mask
2594
- */
2595
- function maskYamlQuotedContent(line) {
2596
- const masked = line.split('');
2597
- let quote;
2598
- let escaped = false;
2599
- for (let index = 0; index < line.length; index += 1) {
2600
- const character = line[index];
2601
- if (quote === '"') {
2602
- masked[index] = ' ';
2603
- if (escaped) escaped = false;
2604
- else if (character === '\\') escaped = true;
2605
- else if (character === '"') quote = undefined;
2606
- } else if (quote === "'") {
2607
- masked[index] = ' ';
2608
- if (character === "'") {
2609
- if (line[index + 1] === "'") {
2610
- masked[index + 1] = ' ';
2611
- index += 1;
2612
- } else {
2613
- quote = undefined;
2614
- }
2615
- }
2616
- } else if (character === '"' || character === "'") {
2617
- masked[index] = ' ';
2618
- quote = character;
2619
- }
2620
- }
2621
- return masked.join('');
2622
- }
2623
-
2624
- /**
2625
- * Decode the scalar spelling accepted for an action use.
2626
- *
2627
- * @param {string} value YAML scalar source
2628
- * @param {string} code diagnostic code
2629
- * @returns {string} decoded scalar
2630
- */
2631
- function yamlUsesScalar(value, code) {
2632
- const source = value.trim();
2633
- if (source.startsWith("'")) {
2634
- if (source.length < 2 || !source.endsWith("'")) fail(code);
2635
- return source.slice(1, -1).replaceAll("''", "'");
2636
- }
2637
- if (source.startsWith('"')) {
2638
- try {
2639
- const decoded = JSON.parse(source);
2640
- return asString(decoded, code);
2641
- } catch {
2642
- fail(code);
2643
- }
2644
- }
2645
- if (source.length === 0 || /\s/u.test(source)) fail(code);
2646
- return source;
2647
- }
2648
-
2649
- /**
2650
- * Parse one immutable GitHub action or reusable-workflow coordinate.
2651
- *
2652
- * @param {unknown} value coordinate value
2653
- * @param {string} code diagnostic code
2654
- * @returns {{use: string, repository: string, path: string | undefined, commit: string}}
2655
- */
2656
- function parseActionCoordinate(value, code) {
2657
- const use = asString(value, code);
2658
- if (
2659
- Buffer.byteLength(use, 'ascii') !== Buffer.byteLength(use, 'utf8') ||
2660
- Buffer.byteLength(use, 'ascii') > 512
2661
- ) {
2662
- fail(code);
2663
- }
2664
- const separator = use.lastIndexOf('@');
2665
- const revision = use.slice(separator + 1);
2666
- const location = use.slice(0, separator);
2667
- if (separator < 0 || !GITHUB_SHA_PATTERN.test(revision)) fail(code);
2668
- const parts = location.split('/');
2669
- const owner = parts.shift();
2670
- const repositoryName = parts.shift();
2671
- if (!owner || !repositoryName) fail(code);
2672
- const repository = `${owner}/${repositoryName}`;
2673
- githubRepositoryCoordinate(repository, code);
2674
- const path = parts.length === 0 ? undefined : parts.join('/');
2675
- if (path !== undefined) {
2676
- try {
2677
- validatePortablePath(path);
2678
- } catch {
2679
- fail(code);
2680
- }
2681
- }
2682
- return { use, repository, path, commit: `sha1:${revision}` };
2683
- }
2684
-
2685
- /**
2686
- * Derive the action coordinates occurring in one validated YAML document.
2687
- * Block scalars are skipped and flow-style `uses` mappings reject so an action
2688
- * can never disappear behind a line-oriented interpretation.
2689
- *
2690
- * @param {Uint8Array} content exact workflow or action-definition bytes
2691
- * @param {string} code diagnostic code
2692
- * @returns {string[]} action coordinates in occurrence order
2693
- */
2694
- function actionUsesFromYaml(content, code) {
2695
- let source;
2696
- try {
2697
- source = FATAL_UTF8_DECODER.decode(content);
2698
- } catch {
2699
- fail(code);
2700
- }
2701
- if (source.startsWith('\uFEFF')) source = source.slice(1);
2702
- const uses = [];
2703
- let blockIndent;
2704
- for (const physicalLine of source.split('\n')) {
2705
- const line = physicalLine.endsWith('\r')
2706
- ? physicalLine.slice(0, -1)
2707
- : physicalLine;
2708
- if (line.trim().length === 0) continue;
2709
- const indentation = /^ */u.exec(line)?.[0].length ?? 0;
2710
- if (blockIndent !== undefined && indentation > blockIndent) continue;
2711
- blockIndent = undefined;
2712
- if (line.slice(0, indentation).includes('\t')) fail(code);
2713
- const codeLine = yamlCodeLine(line).trimEnd();
2714
- if (codeLine.trim().length === 0) continue;
2715
- const body = codeLine.slice(indentation);
2716
- const match = /^(?:-\s+)?(?:uses|'uses'|"uses")\s*:\s*(.*)$/u.exec(body);
2717
- if (match) {
2718
- const scalar = match[1];
2719
- if (scalar === undefined || /^[|>]/u.test(scalar.trim())) fail(code);
2720
- const parsed = parseActionCoordinate(yamlUsesScalar(scalar, code), code);
2721
- if (!parsed.path?.startsWith('.github/workflows/')) {
2722
- uses.push(parsed.use);
2723
- }
2724
- } else {
2725
- const syntax = maskYamlQuotedContent(body);
2726
- if (
2727
- /(?:^|\s|[{},?]|\[)uses\s*:/u.test(syntax) ||
2728
- /(?:^|[,{])\s*(?:'uses'|"uses")\s*:/u.test(body) ||
2729
- /^\?\s*(?:uses|'uses'|"uses")\s*$/u.test(body)
2730
- ) {
2731
- fail(code);
2732
- }
2733
- }
2734
- if (/:\s*[|>](?:[1-9][+-]?|[+-][1-9]?|[+-])?\s*$/u.test(codeLine)) {
2735
- blockIndent = indentation;
2736
- }
2737
- }
2738
- return uses;
2739
- }
2740
-
2741
- /**
2742
- * Parse one SHA-1 Git tree object.
2743
- *
2744
- * @param {Buffer} content exact tree-object body
2745
- * @param {string} code diagnostic code
2746
- * @returns {{mode: string, name: Buffer, objectId: string}[]} entries
2747
- */
2748
- function parseGitTree(content, code) {
2749
- const entries = [];
2750
- const names = new Set();
2751
- let offset = 0;
2752
- while (offset < content.length) {
2753
- const space = content.indexOf(0x20, offset);
2754
- const nul = content.indexOf(0x00, space + 1);
2755
- if (space <= offset || nul <= space + 1 || nul + 21 > content.length) {
2756
- fail(code);
2757
- }
2758
- const mode = content.subarray(offset, space).toString('ascii');
2759
- const name = content.subarray(space + 1, nul);
2760
- if (!/^(?:40000|100644|100755|120000|160000)$/u.test(mode)) fail(code);
2761
- if (name.includes(0x2f)) fail(code);
2762
- const nameKey = name.toString('hex');
2763
- if (names.has(nameKey)) fail(code);
2764
- names.add(nameKey);
2765
- const objectId = `sha1:${content.subarray(nul + 1, nul + 21).toString('hex')}`;
2766
- entries.push({ mode, name, objectId });
2767
- offset = nul + 21;
2768
- }
2769
- return entries;
2770
- }
2771
-
2772
- /**
2773
- * Resolve one action definition through its authenticated commit/tree proof.
2774
- *
2775
- * @param {Record<string, unknown>} evidence action evidence row
2776
- * @param {ReturnType<typeof parseActionCoordinate>} coordinate parsed use
2777
- * @param {string} code diagnostic code
2778
- * @returns {Buffer} exact selected action-definition bytes
2779
- */
2780
- function resolveActionDefinition(evidence, coordinate, code) {
2781
- const commitBytes = bytes(evidence.commitBytes, code);
2782
- if (!gitObjectMatches(coordinate.commit, 'commit', commitBytes)) fail(code);
2783
- const treeHeader = /^tree ([0-9a-f]{40})\n/u.exec(
2784
- commitBytes.subarray(0, 46).toString('ascii'),
2785
- );
2786
- if (!treeHeader?.[1]) fail(code);
2787
- const segments = coordinate.path?.split('/') ?? [];
2788
- const treeObjects = asArray(evidence.treeObjects, code);
2789
- if (treeObjects.length !== segments.length + 1) fail(code);
2790
- let expectedTree = `sha1:${treeHeader[1]}`;
2791
- /** @type {{mode: string, name: Buffer, objectId: string}[]} */
2792
- let selectedEntries = [];
2793
- for (let index = 0; index < treeObjects.length; index += 1) {
2794
- const object = ledger(treeObjects[index], ['objectId', 'bytes'], code);
2795
- const objectId = gitObjectId(object.objectId, code);
2796
- equal(objectId, expectedTree, code);
2797
- const treeBytes = bytes(object.bytes, code);
2798
- if (
2799
- !objectId.startsWith('sha1:') ||
2800
- !gitObjectMatches(objectId, 'tree', treeBytes)
2801
- ) {
2802
- fail(code);
2803
- }
2804
- selectedEntries = parseGitTree(treeBytes, code);
2805
- const segment = segments[index];
2806
- if (segment === undefined) continue;
2807
- const encoded = Buffer.from(segment, 'utf8');
2808
- const matches = selectedEntries.filter(({ name }) => name.equals(encoded));
2809
- const selected = matches[0];
2810
- if (matches.length !== 1 || !selected || selected.mode !== '40000') {
2811
- fail(code);
2812
- }
2813
- expectedTree = selected.objectId;
2814
- }
2815
- const candidates = selectedEntries.filter(
2816
- ({ name }) =>
2817
- name.equals(Buffer.from('action.yml')) ||
2818
- name.equals(Buffer.from('action.yaml')),
2819
- );
2820
- const selected = candidates[0];
2821
- if (
2822
- candidates.length !== 1 ||
2823
- !selected ||
2824
- (selected.mode !== '100644' && selected.mode !== '100755')
2825
- ) {
2826
- fail(code);
2827
- }
2828
- const definitionBytes = bytes(evidence.definitionBytes, code);
2829
- if (!gitObjectMatches(selected.objectId, 'blob', definitionBytes)) fail(code);
2830
- return definitionBytes;
2831
- }
2832
-
2833
- /**
2834
- * Validate exact workflow-file and action-definition closure.
2835
- *
2836
- * @param {Record<string, unknown>} provenance provenance record
2837
- * @param {Record<string, unknown>} context trusted context
2838
- * @param {SourceState} sourceState source state
2839
- * @returns {{workflowFiles: Record<string, unknown>[], actions: Record<string, unknown>[]}}
2840
- */
2841
- function validateWorkflowEvidence(provenance, context, sourceState) {
2842
- const code = 'PROVENANCE_WORKFLOW_INVALID';
2843
- const workflowRows = asArray(context.workflowFiles, code).map(
2844
- (candidate, index) => {
2845
- const row = ledger(
2846
- candidate,
2847
- ['role', 'repositoryId', 'path', 'commit', 'identitySource', 'bytes'],
2848
- code,
2849
- );
2850
- equal(row.role, WORKFLOW_ROLES[index], code);
2851
- equal(row.path, WORKFLOW_PATHS[index], code);
2852
- equal(
2853
- row.identitySource,
2854
- index === 0 ? 'declared-graph' : 'locked-release',
2855
- code,
2856
- );
2857
- if (index === 0)
2858
- equal(
2859
- row.repositoryId,
2860
- sourceState.repositoryContext.repositoryId,
2861
- code,
2862
- );
2863
- validatePortablePath(asString(row.path, code));
2864
- const fileBytes = bytes(row.bytes, code);
2865
- return {
2866
- evidence: {
2867
- role: row.role,
2868
- repositoryId: row.repositoryId,
2869
- path: row.path,
2870
- commit: gitObjectId(row.commit, code),
2871
- fileDigest: sha256Tagged(fileBytes),
2872
- identitySource: row.identitySource,
2873
- },
2874
- bytes: fileBytes,
2875
- };
2876
- },
2877
- );
2878
- if (workflowRows.length !== 4) fail(code);
2879
- const contextFiles = workflowRows.map(({ evidence }) => evidence);
2880
- jcsEqual(provenance.workflowFiles, contextFiles, code);
2881
- const rawActions = asArray(context.actions, code);
2882
- if (rawActions.length < 1 || rawActions.length > 32) fail(code);
2883
- const evidenceByUse = new Map();
2884
- for (const candidate of rawActions) {
2885
- const row = ledger(
2886
- candidate,
2887
- ['use', 'commitBytes', 'treeObjects', 'definitionBytes'],
2888
- code,
2889
- );
2890
- const coordinate = parseActionCoordinate(row.use, code);
2891
- if (evidenceByUse.has(coordinate.use)) fail(code);
2892
- evidenceByUse.set(coordinate.use, { row, coordinate });
2893
- }
2894
- const pending = workflowRows.flatMap(({ bytes: content }) =>
2895
- actionUsesFromYaml(content, code),
2896
- );
2897
- const visited = new Set();
2898
- const contextActions = [];
2899
- while (pending.length > 0) {
2900
- const use = pending.shift();
2901
- if (use === undefined || visited.has(use)) continue;
2902
- if (visited.size >= 32) fail(code);
2903
- const action = evidenceByUse.get(use);
2904
- if (!action) fail(code);
2905
- visited.add(use);
2906
- const definitionBytes = resolveActionDefinition(
2907
- action.row,
2908
- action.coordinate,
2909
- code,
2910
- );
2911
- contextActions.push({
2912
- use,
2913
- commit: action.coordinate.commit,
2914
- actionDefinitionDigest: digestActionDefinitionBlob(definitionBytes),
2915
- });
2916
- pending.push(...actionUsesFromYaml(definitionBytes, code));
2917
- }
2918
- if (visited.size !== evidenceByUse.size) fail(code);
2919
- const sortedActions = contextActions.sort(compareJcs);
2920
- jcsEqual(provenance.actionPins, sortedActions, code);
2921
- return { workflowFiles: contextFiles, actions: sortedActions };
2922
- }
2923
-
2924
- /**
2925
- * @param {string} segment NFC artifact path segment
2926
- * @returns {string} RFC 3986 segment encoding
2927
- */
2928
- function encodeRouteSegment(segment) {
2929
- let encoded = '';
2930
- for (const byte of Buffer.from(segment, 'utf8')) {
2931
- const character = String.fromCharCode(byte);
2932
- encoded += /^[A-Za-z0-9._~-]$/u.test(character)
2933
- ? character
2934
- : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
2935
- }
2936
- return encoded;
2937
- }
2938
-
2939
- /**
2940
- * @param {string} path artifact backing path
2941
- * @param {string} basePath directory-form public base path
2942
- * @param {string} profile route normalization profile
2943
- * @returns {string} projected canonical public route
2944
- */
2945
- function projectArtifactRoute(path, basePath, profile) {
2946
- validatePortablePath(path);
2947
- if (!basePath.startsWith('/') || !basePath.endsWith('/')) {
2948
- fail('ARTIFACT_REDIRECT_INVALID');
2949
- }
2950
- const segments = path.split('/').map(encodeRouteSegment);
2951
- if (profile === 'directory-index' && segments.at(-1) === 'index.html') {
2952
- segments.pop();
2953
- const suffix = segments.length === 0 ? '' : `${segments.join('/')}/`;
2954
- return `${basePath}${suffix}`;
2955
- }
2956
- if (profile !== 'directory-index' && profile !== 'explicit-file') {
2957
- fail('ARTIFACT_REDIRECT_INVALID');
2958
- }
2959
- return `${basePath}${segments.join('/')}`;
2960
- }
2961
-
2962
- /**
2963
- * @param {string} targetRoute redirect target
2964
- * @returns {Buffer} exact generated redirect bytes
2965
- */
2966
- function redirectBytes(targetRoute) {
2967
- const escaped = targetRoute
2968
- .replaceAll('&', '&amp;')
2969
- .replaceAll('<', '&lt;')
2970
- .replaceAll('>', '&gt;')
2971
- .replaceAll('"', '&quot;')
2972
- .replaceAll("'", '&#39;');
2973
- return Buffer.from(
2974
- `<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta http-equiv="refresh" content="0;url=${escaped}">\n<link rel="canonical" href="${escaped}">\n<title>Redirecting...</title>\n</head>\n<body>\n<p>This page has moved to <a href="${escaped}">${escaped}</a>.</p>\n</body>\n</html>\n`,
2975
- 'utf8',
2976
- );
2977
- }
2978
-
2979
- /**
2980
- * Validate and decode the exact frozen handoff.
2981
- *
2982
- * @param {Record<string, unknown>} records semantic record bundle
2983
- * @param {Record<string, unknown>} context trusted context
2984
- * @returns {{payloads: Map<string, Buffer>, envelope: ReturnType<typeof validateFrozenEnvelope>}}
2985
- */
2986
- function validateFrozen(records, context) {
2987
- const code = 'FROZEN_ENVELOPE_SEMANTICS_INVALID';
2988
- asObject(records, code);
2989
- let envelope;
2990
- try {
2991
- envelope = validateFrozenEnvelope(bytes(context.frozenEnvelopeBytes, code));
2992
- } catch {
2993
- fail(code);
2994
- }
2995
- const payloads = new Map();
2996
- for (const record of envelope.records) {
2997
- if (record.kind !== 0x01) continue;
2998
- if (payloads.has(record.path)) fail(code);
2999
- payloads.set(record.path, Buffer.from(record.content));
3000
- }
3001
- return { payloads, envelope };
3002
- }
3003
-
3004
- /**
3005
- * Check byte-semantic parity after each external record has passed its owning
3006
- * semantic validator, so a malformed record receives its precise diagnostic.
3007
- *
3008
- * @param {Record<string, unknown>} records semantic record bundle
3009
- * @param {ReturnType<typeof validateFrozenEnvelope>} envelope frozen records
3010
- * @returns {void}
3011
- */
3012
- function validateFrozenRecordParity(records, envelope) {
3013
- const code = 'FROZEN_ENVELOPE_SEMANTICS_INVALID';
3014
- jcsEqual(envelope.artifactManifest, records.artifactManifest, code);
3015
- jcsEqual(envelope.buildProvenance, records.buildProvenance, code);
3016
- jcsEqual(envelope.sbom, records.sbom, code);
3017
- }
3018
-
3019
- /**
3020
- * @typedef {{
3021
- * output: Record<string, unknown>,
3022
- * path: string,
3023
- * kind: string,
3024
- * bytes: Buffer,
3025
- * manifestEntry: Record<string, unknown>
3026
- * }} ArtifactOutput
3027
- */
3028
-
3029
- /**
3030
- * Validate the output-index context and construct exact manifest entries.
3031
- *
3032
- * @param {Record<string, unknown>} context context
3033
- * @param {Map<string, Buffer>} payloads frozen payload bytes
3034
- * @returns {ArtifactOutput[]} outputs in context order
3035
- */
3036
- function validateArtifactOutputs(context, payloads) {
3037
- const code = 'ARTIFACT_INVENTORY_INVALID';
3038
- const rawOutputs = asArray(context.artifactOutputs, code);
3039
- if (
3040
- rawOutputs.length < 1 ||
3041
- rawOutputs.length > MAX_ARTIFACT_FILES ||
3042
- rawOutputs.length !== payloads.size
3043
- ) {
3044
- fail(code);
3045
- }
3046
- const paths = new Set();
3047
- const collisions = new Set();
3048
- return rawOutputs.map((candidate) => {
3049
- const output = asObject(candidate, code);
3050
- const kind = asString(output.kind, code);
3051
- const common = ['kind', 'path', 'mediaType', 'sourceKind'];
3052
- if (kind === 'route') {
3053
- ledger(output, [...common, 'routeClass'], code, [
3054
- 'stableContentId',
3055
- 'sourceRevision',
3056
- 'themeAssetPath',
3057
- ]);
3058
- } else if (kind === 'asset') {
3059
- ledger(output, [...common, 'immutable'], code, ['themeAssetPath']);
3060
- } else {
3061
- fail(code);
3062
- }
3063
- const path = asString(output.path, code);
3064
- const collision = validatePortablePath(path);
3065
- if (paths.has(path) || collisions.has(collision)) fail(code);
3066
- paths.add(path);
3067
- collisions.add(collision);
3068
- const content = payloads.get(path);
3069
- if (!content) fail(code);
3070
- const mediaType = asString(output.mediaType, code);
3071
- if (!MANIFEST_MEDIA_TYPES.includes(mediaType)) fail(code);
3072
- const commonEntry = {
3073
- path,
3074
- mediaType,
3075
- byteLength: String(content.length),
3076
- sha256: sha256Tagged(content),
3077
- };
3078
- let manifestEntry;
3079
- if (kind === 'route') {
3080
- manifestEntry = {
3081
- ...commonEntry,
3082
- routeClass: output.routeClass,
3083
- ...(Object.hasOwn(output, 'stableContentId')
3084
- ? { stableContentId: output.stableContentId }
3085
- : {}),
3086
- ...(Object.hasOwn(output, 'sourceRevision')
3087
- ? { sourceRevision: output.sourceRevision }
3088
- : {}),
3089
- interactionBearing: false,
3090
- };
3091
- } else {
3092
- manifestEntry = { ...commonEntry, immutable: output.immutable };
3093
- }
3094
- return { output, path, kind, bytes: content, manifestEntry };
3095
- });
3096
- }
3097
-
3098
- /**
3099
- * Validate distinct empty roots and two-run payload/index/diagnostic identity.
3100
- *
3101
- * @param {unknown} value reproducibility runs
3102
- * @param {ArtifactOutput[]} outputs frozen outputs
3103
- * @param {Record<string, unknown>} manifest manifest
3104
- * @returns {void}
3105
- */
3106
- function validateReproducibility(value, outputs, manifest) {
3107
- const code = 'ARTIFACT_REPRODUCIBILITY_INVALID';
3108
- const runs = asArray(value, code);
3109
- if (runs.length !== 2) fail(code);
3110
- const expectedFiles = [...outputs]
3111
- .sort((left, right) => compareUtf8(left.path, right.path))
3112
- .map(({ path, bytes: content }) => ({ path, bytes: content }));
3113
- const expectedIndexProjection = outputs.map(({ output }) => output);
3114
- const roots = new Set();
3115
- for (const candidate of runs) {
3116
- const run = ledger(
3117
- candidate,
3118
- [
3119
- 'rootIdentity',
3120
- 'initialEntries',
3121
- 'files',
3122
- 'indexProjection',
3123
- 'findings',
3124
- 'measurements',
3125
- ],
3126
- code,
3127
- );
3128
- const rootIdentity = digest(run.rootIdentity, code);
3129
- if (roots.has(rootIdentity)) fail(code);
3130
- roots.add(rootIdentity);
3131
- jcsEqual(run.initialEntries, [], code);
3132
- const files = asArray(run.files, code).map((file) =>
3133
- ledger(file, ['path', 'bytes'], code),
3134
- );
3135
- if (files.length !== expectedFiles.length) fail(code);
3136
- for (let index = 0; index < files.length; index += 1) {
3137
- const actual = files[index];
3138
- const expected = expectedFiles[index];
3139
- if (!actual || !expected) fail(code);
3140
- equal(actual.path, expected.path, code);
3141
- if (!bytes(actual.bytes, code).equals(expected.bytes)) fail(code);
3142
- }
3143
- jcsEqual(run.indexProjection, expectedIndexProjection, code);
3144
- jcsEqual(run.findings, manifest.findings, code);
3145
- jcsEqual(run.measurements, manifest.measurements, code);
3146
- }
3147
- equal(manifest.reproducibilityClass, 'byte-identical', code);
3148
- }
3149
-
3150
- /**
3151
- * Build exact included/excluded source rows.
3152
- *
3153
- * @param {SourceState} sourceState validated source state
3154
- * @returns {{included: Record<string, unknown>[], excluded: Record<string, unknown>[]}}
3155
- */
3156
- function sourceInventory(sourceState) {
3157
- const code = 'ARTIFACT_SOURCE_PARTITION_INVALID';
3158
- const revision = sourceState.repositoryContext.sourceRevision;
3159
- const included = sourceState.includedSources.map((row) => ({
3160
- path: row.path,
3161
- sha256: row.sha256,
3162
- sourceRevision: revision,
3163
- role: row.role,
3164
- }));
3165
- const exclusionByPath = new Map();
3166
- for (const candidate of sourceState.exclusions) {
3167
- const exclusion = ledger(candidate, ['path', 'ruleId', 'reason'], code);
3168
- const path = asString(exclusion.path, code);
3169
- if (exclusionByPath.has(path)) fail(code);
3170
- const reason = asString(exclusion.reason, code);
3171
- if (
3172
- ![
3173
- 'not-referenced-by-build-input',
3174
- 'deferred-capability-absent',
3175
- 'non-artifact-source',
3176
- 'policy-excluded',
3177
- ].includes(reason)
3178
- ) {
3179
- fail(code);
3180
- }
3181
- exclusionByPath.set(path, exclusion);
3182
- }
3183
- const includedPaths = new Set(
3184
- included.map((row) => asString(row.path, code)),
3185
- );
3186
- const excluded = [];
3187
- for (const [path, file] of sourceState.treeFiles) {
3188
- if (includedPaths.has(path)) {
3189
- if (exclusionByPath.has(path)) fail(code);
3190
- continue;
3191
- }
3192
- const exclusion = exclusionByPath.get(path);
3193
- if (!exclusion) fail(code);
3194
- excluded.push({
3195
- path,
3196
- sha256: sha256Tagged(file.bytes),
3197
- ruleId: exclusion.ruleId,
3198
- reason: exclusion.reason,
3199
- });
3200
- exclusionByPath.delete(path);
3201
- }
3202
- if (exclusionByPath.size !== 0) fail(code);
3203
- included.sort(compareJcs);
3204
- excluded.sort(compareJcs);
3205
- return { included, excluded };
3206
- }
3207
-
3208
- /**
3209
- * Validate and derive redirect rows.
3210
- *
3211
- * @param {Record<string, unknown>} buildInput build input
3212
- * @param {SourceState} sourceState source state
3213
- * @param {ArtifactOutput[]} outputs artifact outputs
3214
- * @returns {Record<string, unknown>[]} exact redirect rows
3215
- */
3216
- function expectedRedirects(buildInput, sourceState, outputs) {
3217
- const code = 'ARTIFACT_REDIRECT_INVALID';
3218
- const contentRoutes = asArray(
3219
- sourceState.repositoryContext.contentRoutes,
3220
- code,
3221
- ).map((row) => ledger(row, ['contentId', 'route'], code));
3222
- const routesByContent = new Map();
3223
- const canonicalRoutes = new Set();
3224
- for (const row of contentRoutes) {
3225
- const id = asString(row.contentId, code);
3226
- const route = asString(row.route, code);
3227
- if (routesByContent.has(id) || canonicalRoutes.has(route)) fail(code);
3228
- routesByContent.set(id, route);
3229
- canonicalRoutes.add(route);
3230
- }
3231
- const manifestRoutes = outputs.filter(({ kind }) => kind === 'route');
3232
- const normalization = asString(
3233
- sourceState.repositoryContext.routeNormalizationProfile,
3234
- code,
3235
- );
3236
- const basePath = asString(buildInput.basePath, code);
3237
- const redirects = [];
3238
- const sources = new Set();
3239
- const content = asArray(buildInput.content, code);
3240
- if (contentRoutes.length !== content.length) fail(code);
3241
- const selectedContentIds = new Set();
3242
- for (const candidate of content) {
3243
- const record = asObject(candidate, code);
3244
- const frontmatter = asObject(record.frontmatter, code);
3245
- const contentId = asString(frontmatter.id, code);
3246
- if (selectedContentIds.has(contentId)) fail(code);
3247
- selectedContentIds.add(contentId);
3248
- const targetRoute = routesByContent.get(contentId);
3249
- if (!targetRoute) fail(code);
3250
- if (Object.hasOwn(frontmatter, 'route'))
3251
- equal(frontmatter.route, targetRoute, code);
3252
- const canonicalOutput = manifestRoutes.filter(
3253
- ({ output, path }) =>
3254
- output.stableContentId === contentId &&
3255
- projectArtifactRoute(path, basePath, normalization) === targetRoute,
3256
- );
3257
- if (canonicalOutput.length !== 1) fail(code);
3258
- const selectedOutput = canonicalOutput[0];
3259
- if (!selectedOutput) fail(code);
3260
- equal(selectedOutput.output.sourceKind, 'generated', code);
3261
- equal(
3262
- selectedOutput.output.sourceRevision,
3263
- buildInput.sourceRevision,
3264
- code,
3265
- );
3266
- for (const sourceRouteValue of asArray(frontmatter.redirects, code)) {
3267
- const sourceRoute = asString(sourceRouteValue, code);
3268
- if (sources.has(sourceRoute) || canonicalRoutes.has(sourceRoute)) {
3269
- fail(code);
3270
- }
3271
- sources.add(sourceRoute);
3272
- const matches = manifestRoutes.filter(
3273
- ({ path }) =>
3274
- projectArtifactRoute(path, basePath, normalization) === sourceRoute,
3275
- );
3276
- if (matches.length !== 1) fail(code);
3277
- const backing = matches[0];
3278
- if (!backing || !backing.bytes.equals(redirectBytes(targetRoute)))
3279
- fail(code);
3280
- redirects.push({
3281
- sourceRoute,
3282
- targetRoute,
3283
- status: 200,
3284
- backingPath: backing.path,
3285
- sha256: sha256Tagged(backing.bytes),
3286
- });
3287
- }
3288
- }
3289
- if (
3290
- selectedContentIds.size !== routesByContent.size ||
3291
- [...routesByContent.keys()].some((id) => !selectedContentIds.has(id))
3292
- ) {
3293
- fail(code);
3294
- }
3295
- if (redirects.some((row) => sources.has(asString(row.targetRoute, code))))
3296
- fail(code);
3297
- redirects.sort((left, right) =>
3298
- compareUtf8(
3299
- asString(left.sourceRoute, code),
3300
- asString(right.sourceRoute, code),
3301
- ),
3302
- );
3303
- return redirects;
3304
- }
3305
-
3306
- /**
3307
- * Validate exact manifest inventory, identities, policy facts, and digests.
3308
- *
3309
- * @param {Record<string, unknown>} manifest artifact manifest
3310
- * @param {Record<string, unknown>} lock lock record
3311
- * @param {Record<string, unknown>} buildInput build input
3312
- * @param {Record<string, unknown>} provenance build provenance
3313
- * @param {Record<string, unknown>} context trusted context
3314
- * @param {LockState} lockState package state
3315
- * @param {SourceState} sourceState source state
3316
- * @param {{binding: Record<string, unknown>}} workload workload state
3317
- * @param {Record<string, unknown>[]} workflowFiles workflow evidence
3318
- * @param {Map<string, Buffer>} payloads frozen payload bytes
3319
- * @returns {{outputs: ArtifactOutput[], buildPolicyDecision: Record<string, unknown>}}
3320
- */
3321
- function validateArtifactManifest(
3322
- manifest,
3323
- lock,
3324
- buildInput,
3325
- provenance,
3326
- context,
3327
- lockState,
3328
- sourceState,
3329
- workload,
3330
- workflowFiles,
3331
- payloads,
3332
- ) {
3333
- const code = 'ARTIFACT_MANIFEST_INVALID';
3334
- ledger(manifest, MANIFEST_KEYS, code);
3335
- equal(manifest.schemaId, 'urn:gala:schema:artifact-manifest:2.0.0', code);
3336
- equal(manifest.schemaVersion, '2.0.0', code);
3337
- const outputs = validateArtifactOutputs(context, payloads);
3338
- const expectedRoutes = outputs
3339
- .filter(({ kind }) => kind === 'route')
3340
- .map(({ manifestEntry }) => manifestEntry)
3341
- .sort((left, right) =>
3342
- compareUtf8(asString(left.path, code), asString(right.path, code)),
3343
- );
3344
- const expectedAssets = outputs
3345
- .filter(({ kind }) => kind === 'asset')
3346
- .map(({ manifestEntry }) => manifestEntry)
3347
- .sort((left, right) =>
3348
- compareUtf8(asString(left.path, code), asString(right.path, code)),
3349
- );
3350
- jcsEqual(manifest.routes, expectedRoutes, 'ARTIFACT_INVENTORY_INVALID');
3351
- jcsEqual(manifest.assets, expectedAssets, 'ARTIFACT_INVENTORY_INVALID');
3352
- let artifactBytes = 0n;
3353
- for (const output of outputs) artifactBytes += BigInt(output.bytes.length);
3354
- equal(
3355
- positiveInt64(manifest.artifactFileCount, 'ARTIFACT_INVENTORY_INVALID'),
3356
- BigInt(outputs.length),
3357
- 'ARTIFACT_INVENTORY_INVALID',
3358
- );
3359
- equal(
3360
- positiveInt64(manifest.artifactByteCount, 'ARTIFACT_INVENTORY_INVALID'),
3361
- artifactBytes,
3362
- 'ARTIFACT_INVENTORY_INVALID',
3363
- );
3364
- if (
3365
- outputs.length > MAX_ARTIFACT_FILES ||
3366
- artifactBytes > MAX_ARTIFACT_BYTES
3367
- ) {
3368
- fail('ARTIFACT_INVENTORY_INVALID');
3369
- }
3370
- const artifactEntries = outputs
3371
- .map(({ manifestEntry }) => ({
3372
- path: manifestEntry.path,
3373
- byteLength: manifestEntry.byteLength,
3374
- sha256: manifestEntry.sha256,
3375
- }))
3376
- .sort((left, right) =>
3377
- compareUtf8(asString(left.path, code), asString(right.path, code)),
3378
- );
3379
- equal(
3380
- manifest.artifactDigest,
3381
- digestProfile('artifact', artifactEntries, 'ARTIFACT_DIGEST_INVALID'),
3382
- 'ARTIFACT_DIGEST_INVALID',
3383
- );
3384
-
3385
- const repository = asObject(buildInput.repository, code);
3386
- equal(manifest.repositoryNodeId, repository.repositoryId, code);
3387
- equal(manifest.sourceCommit, buildInput.sourceRevision, code);
3388
- equal(manifest.buildInputDigest, buildInput.inputDigest, code);
3389
- equal(manifest.generatedAt, buildInput.buildEpoch, code);
3390
- equal(manifest.buildInputContractVersion, buildInput.contractVersion, code);
3391
- const sourceIdentity = {
3392
- provider: 'github',
3393
- repository: workload.binding.repository,
3394
- repositoryId: repository.repositoryId,
3395
- repositoryOwnerId: repository.repositoryOwnerId,
3396
- commit: buildInput.sourceRevision,
3397
- treeDigest: repository.rootDigest,
3398
- };
3399
- jcsEqual(manifest.sourceIdentity, sourceIdentity, code);
3400
- const publishers = asArray(lock.publisher, code).map((row) =>
3401
- asObject(row, code),
3402
- );
3403
- jcsEqual(manifest.builder, packageIdentity(publishers[0] ?? {}, code), code);
3404
- const expectedComposition = {
3405
- schemas: packageIdentity(asObject(lock.schemas, code), code),
3406
- template: packageIdentity(asObject(lock.template, code), code),
3407
- theme: packageIdentity(asObject(lock.theme, code), code),
3408
- publisher: publishers.map((row) => packageIdentity(row, code)),
3409
- enabledModuleConfigurationDigests: [],
3410
- };
3411
- jcsEqual(manifest.composition, expectedComposition, code);
3412
- equal(
3413
- manifest.workflowIdentity,
3414
- digestProfile(
3415
- 'buildWorkflowIdentity',
3416
- { workflowFiles },
3417
- 'PROVENANCE_WORKFLOW_INVALID',
3418
- ),
3419
- 'PROVENANCE_WORKFLOW_INVALID',
3420
- );
3421
- const sandbox = asObject(provenance.sandbox, 'PROVENANCE_SANDBOX_INVALID');
3422
- const expectedTools = [
3423
- {
3424
- kind: 'runtime',
3425
- name: 'node',
3426
- version: '24.18.0',
3427
- digest: sandbox.nodeExecutableDigest,
3428
- },
3429
- {
3430
- kind: 'runtime',
3431
- name: 'npm',
3432
- version: '11.16.0',
3433
- digest: sandbox.npmExecutableDigest,
3434
- },
3435
- ...lockState.all.map((row) => {
3436
- const identity = packageIdentity(row, code);
3437
- return {
3438
- kind: 'package',
3439
- package: identity.package,
3440
- version: identity.version,
3441
- digest: identity.integrity,
3442
- };
3443
- }),
3444
- ].sort(compareJcs);
3445
- jcsEqual(
3446
- manifest.buildToolVersions,
3447
- expectedTools,
3448
- 'ARTIFACT_TOOL_IDENTITY_INVALID',
3449
- );
3450
-
3451
- const findings = asArray(manifest.findings, 'ARTIFACT_POLICY_INVALID');
3452
- const measurements = asArray(
3453
- manifest.measurements,
3454
- 'ARTIFACT_POLICY_INVALID',
3455
- );
3456
- assertCanonicalSet(findings, 'ARTIFACT_POLICY_INVALID');
3457
- assertCanonicalSet(measurements, 'ARTIFACT_POLICY_INVALID');
3458
- const hasError = findings.some(
3459
- (finding) =>
3460
- asObject(finding, 'ARTIFACT_POLICY_INVALID').severity === 'error',
3461
- );
3462
- const hasWarning = findings.some(
3463
- (finding) =>
3464
- asObject(finding, 'ARTIFACT_POLICY_INVALID').severity === 'warning',
3465
- );
3466
- const expectedResult = hasError
3467
- ? 'fail'
3468
- : hasWarning
3469
- ? 'pass-with-warnings'
3470
- : 'pass';
3471
- equal(manifest.policyResult, expectedResult, 'ARTIFACT_POLICY_INVALID');
3472
- if (expectedResult === 'fail') fail('ARTIFACT_POLICY_INVALID');
3473
- validateReproducibility(context.reproducibilityRuns, outputs, manifest);
3474
- jcsEqual(manifest.declarativeHeaders, [], code);
3475
-
3476
- const inventory = sourceInventory(sourceState);
3477
- jcsEqual(
3478
- manifest.includedSources,
3479
- inventory.included,
3480
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
3481
- );
3482
- jcsEqual(
3483
- manifest.excludedInputs,
3484
- inventory.excluded,
3485
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
3486
- );
3487
- const sourceProjection = {
3488
- includedSources: inventory.included,
3489
- excludedInputs: inventory.excluded,
3490
- };
3491
- equal(
3492
- manifest.sourceInventoryDigest,
3493
- digestProfile(
3494
- 'sourceInventory',
3495
- sourceProjection,
3496
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
3497
- ),
3498
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
3499
- );
3500
- jcsEqual(
3501
- manifest.redirects,
3502
- expectedRedirects(buildInput, sourceState, outputs),
3503
- 'ARTIFACT_REDIRECT_INVALID',
3504
- );
3505
- const validation = ledger(
3506
- manifest.validation,
3507
- ['profile', 'version', 'findingCount', 'evidenceDigest'],
3508
- 'ARTIFACT_VALIDATION_INVALID',
3509
- );
3510
- equal(
3511
- validation.profile,
3512
- 'gala-artifact-validation-v2',
3513
- 'ARTIFACT_VALIDATION_INVALID',
3514
- );
3515
- equal(validation.version, '2.0.0', 'ARTIFACT_VALIDATION_INVALID');
3516
- equal(
3517
- validation.findingCount,
3518
- findings.length,
3519
- 'ARTIFACT_VALIDATION_INVALID',
3520
- );
3521
- equal(
3522
- validation.evidenceDigest,
3523
- digestProfile(
3524
- 'artifactValidationEvidence',
3525
- manifest,
3526
- 'ARTIFACT_VALIDATION_INVALID',
3527
- ),
3528
- 'ARTIFACT_VALIDATION_INVALID',
3529
- );
3530
- equal(
3531
- manifest.manifestDigest,
3532
- digestProfile('artifactManifest', manifest, 'ARTIFACT_DIGEST_INVALID'),
3533
- 'ARTIFACT_DIGEST_INVALID',
3534
- );
3535
-
3536
- const policyRelease = ledger(
3537
- context.buildPolicyRelease,
3538
- ['policyReleaseId', 'policyProfile', 'policyVersion'],
3539
- 'ARTIFACT_POLICY_INVALID',
3540
- );
3541
- plainLabel(policyRelease.policyProfile, 'ARTIFACT_POLICY_INVALID');
3542
- semver(policyRelease.policyVersion, 'ARTIFACT_POLICY_INVALID');
3543
- const decision = ledger(
3544
- context.buildPolicyDecision,
3545
- [
3546
- 'profile',
3547
- 'policyReleaseId',
3548
- 'policyProfile',
3549
- 'policyVersion',
3550
- 'approvedOverrides',
3551
- 'manifestDigest',
3552
- 'policyResult',
3553
- 'findings',
3554
- 'decisionDigest',
3555
- ],
3556
- 'ARTIFACT_POLICY_INVALID',
3557
- );
3558
- plainLabel(decision.policyProfile, 'ARTIFACT_POLICY_INVALID');
3559
- const expectedDecision = {
3560
- profile: 'gala-build-policy-decision-v2',
3561
- policyReleaseId: policyRelease.policyReleaseId,
3562
- policyProfile: policyRelease.policyProfile,
3563
- policyVersion: policyRelease.policyVersion,
3564
- approvedOverrides: [],
3565
- manifestDigest: manifest.manifestDigest,
3566
- policyResult: manifest.policyResult,
3567
- findings,
3568
- decisionDigest: '',
3569
- };
3570
- expectedDecision.decisionDigest = digestProfile(
3571
- 'buildPolicyDecision',
3572
- expectedDecision,
3573
- 'ARTIFACT_POLICY_INVALID',
3574
- );
3575
- jcsEqual(decision, expectedDecision, 'ARTIFACT_POLICY_INVALID');
3576
- return { outputs, buildPolicyDecision: decision };
3577
- }
3578
-
3579
- /**
3580
- * Validate exact carrier bytes and retained REST facts.
3581
- *
3582
- * @param {unknown} value carrier context rows
3583
- * @param {Record<string, unknown>} asserted asserted workload
3584
- * @returns {Record<string, unknown>[]} carrier evidence by semantic purpose
3585
- */
3586
- function validateCarriers(value, asserted) {
3587
- const code = 'PROVENANCE_CARRIER_INVALID';
3588
- const contexts = asArray(value, code).map((candidate) =>
3589
- ledger(candidate, ['purpose', 'artifactId', 'expiresAt', 'bytes'], code),
3590
- );
3591
- if (contexts.length !== 2) fail(code);
3592
- const expectedPurposes = ['verified-inputs', 'unfrozen-output'];
3593
- const runAttempt = asserted.runAttempt;
3594
- if (
3595
- typeof runAttempt !== 'number' ||
3596
- !Number.isInteger(runAttempt) ||
3597
- runAttempt < 1 ||
3598
- runAttempt > 51
3599
- ) {
3600
- fail(code);
3601
- }
3602
- return contexts.map((carrier, index) => {
3603
- const purpose = expectedPurposes[index];
3604
- equal(carrier.purpose, purpose, code);
3605
- const content = bytes(carrier.bytes, code);
3606
- return {
3607
- purpose,
3608
- artifactId: githubPositiveDecimal(carrier.artifactId, code),
3609
- name: `gala-r${asString(asserted.runId, code)}-a${String(runAttempt)}-${purpose}-v2.bin`,
3610
- byteCount: String(content.length),
3611
- digest: sha256Tagged(content),
3612
- expiresAt: timestamp(carrier.expiresAt, code),
3613
- };
3614
- });
3615
- }
3616
-
3617
- /**
3618
- * Validate runner compatibility and exact executable/sandbox evidence.
3619
- *
3620
- * @param {Record<string, unknown>} provenance provenance
3621
- * @param {Record<string, unknown>} context trusted context
3622
- * @param {Record<string, unknown>} buildInput build input
3623
- * @returns {Record<string, unknown>} validated sandbox
3624
- */
3625
- function validateSandbox(provenance, context, buildInput) {
3626
- const code = 'PROVENANCE_SANDBOX_INVALID';
3627
- const runner = ledger(
3628
- context.runner,
3629
- [
3630
- 'compatibilityRow',
3631
- 'nodeExecutableBytes',
3632
- 'npmExecutableBytes',
3633
- 'networkPolicyDigest',
3634
- 'filesystemPolicyDigest',
3635
- 'sandboxPolicyDigest',
3636
- 'toolCatalogDigest',
3637
- ],
3638
- code,
3639
- );
3640
- const row = ledger(
3641
- runner.compatibilityRow,
3642
- [
3643
- 'profile',
3644
- 'runnerEnvironment',
3645
- 'runnerImageRelease',
3646
- 'softwareManifestDigest',
3647
- 'sandboxPolicyDigest',
3648
- 'toolCatalogDigest',
3649
- 'fixtureDigest',
3650
- 'evidenceDigest',
3651
- 'rowDigest',
3652
- ],
3653
- code,
3654
- );
3655
- equal(row.profile, 'gala-github-hosted-runner-compatibility-v2', code);
3656
- equal(row.runnerEnvironment, 'github-hosted', code);
3657
- for (const key of [
3658
- 'softwareManifestDigest',
3659
- 'sandboxPolicyDigest',
3660
- 'toolCatalogDigest',
3661
- 'fixtureDigest',
3662
- 'evidenceDigest',
3663
- 'rowDigest',
3664
- ]) {
3665
- digest(row[key], code);
3666
- }
3667
- equal(row.sandboxPolicyDigest, runner.sandboxPolicyDigest, code);
3668
- equal(row.toolCatalogDigest, runner.toolCatalogDigest, code);
3669
- equal(
3670
- row.rowDigest,
3671
- digestProfile('githubHostedRunnerCompatibility', row, code),
3672
- code,
3673
- );
3674
- const sandbox = ledger(
3675
- provenance.sandbox,
3676
- [
3677
- 'runnerEnvironment',
3678
- 'runnerImageRelease',
3679
- 'runnerImageReleaseDigest',
3680
- 'nodeVersion',
3681
- 'nodeExecutableDigest',
3682
- 'npmVersion',
3683
- 'npmExecutableDigest',
3684
- 'locale',
3685
- 'timezone',
3686
- 'sourceDateEpoch',
3687
- 'networkPolicyDigest',
3688
- 'filesystemPolicyDigest',
3689
- ],
3690
- code,
3691
- );
3692
- const expected = {
3693
- runnerEnvironment: 'github-hosted',
3694
- runnerImageRelease: row.runnerImageRelease,
3695
- runnerImageReleaseDigest: row.rowDigest,
3696
- nodeVersion: '24.18.0',
3697
- nodeExecutableDigest: sha256Tagged(bytes(runner.nodeExecutableBytes, code)),
3698
- npmVersion: '11.16.0',
3699
- npmExecutableDigest: sha256Tagged(bytes(runner.npmExecutableBytes, code)),
3700
- locale: 'C.UTF-8',
3701
- timezone: 'UTC',
3702
- sourceDateEpoch: buildInput.buildEpoch,
3703
- networkPolicyDigest: digest(runner.networkPolicyDigest, code),
3704
- filesystemPolicyDigest: digest(runner.filesystemPolicyDigest, code),
3705
- };
3706
- jcsEqual(sandbox, expected, code);
3707
- return sandbox;
3708
- }
3709
-
3710
- /**
3711
- * Derive and validate the per-artifact license rows.
3712
- *
3713
- * @param {ArtifactOutput[]} outputs artifact outputs
3714
- * @param {Record<string, unknown>} context trusted context
3715
- * @returns {{conclusions: Record<string, unknown>[], expressions: string[]}}
3716
- */
3717
- function validateArtifactLicenses(outputs, context) {
3718
- const code = 'PROVENANCE_LICENSE_INVALID';
3719
- const themeContract = asObject(context.themeContract, code);
3720
- const themeAssets = asArray(themeContract.assets, code).map((candidate) =>
3721
- asObject(candidate, code),
3722
- );
3723
- const assetsByPath = new Map();
3724
- const expressions = [];
3725
- for (const asset of themeAssets) {
3726
- const path = asString(asset.path, code);
3727
- if (assetsByPath.has(path)) fail(code);
3728
- const expression = asString(asset.license, code);
3729
- try {
3730
- validateSpdxExpression(expression);
3731
- } catch {
3732
- fail(code);
3733
- }
3734
- expressions.push(expression);
3735
- assetsByPath.set(path, asset);
3736
- }
3737
- for (const stylesheet of asArray(themeContract.stylesheets, code)) {
3738
- if (!assetsByPath.has(asString(stylesheet, code))) fail(code);
3739
- }
3740
- const themeFiles = new Map();
3741
- for (const candidate of asArray(context.themeFiles, code)) {
3742
- const file = ledger(candidate, ['path', 'bytes'], code);
3743
- const path = asString(file.path, code);
3744
- if (themeFiles.has(path)) fail(code);
3745
- themeFiles.set(path, bytes(file.bytes, code));
3746
- }
3747
- if (
3748
- themeFiles.size !== assetsByPath.size ||
3749
- [...assetsByPath.keys()].some((path) => !themeFiles.has(path))
3750
- ) {
3751
- fail(code);
3752
- }
3753
- for (const [path, asset] of assetsByPath) {
3754
- const content = themeFiles.get(path);
3755
- if (!content) fail(code);
3756
- equal(
3757
- nonnegativeInt64(asset.byteLength, code),
3758
- BigInt(content.length),
3759
- code,
3760
- );
3761
- equal(asset.sha256, sha256Tagged(content), code);
3762
- }
3763
-
3764
- const usedThemePaths = new Set();
3765
- const conclusions = outputs.map(
3766
- ({ output, path, kind, bytes: outputBytes }) => {
3767
- const sourceKind = asString(output.sourceKind, code);
3768
- if (sourceKind === 'generated') {
3769
- if (Object.hasOwn(output, 'themeAssetPath')) fail(code);
3770
- return {
3771
- artifactPath: path,
3772
- licenseConcluded: 'NOASSERTION',
3773
- sourceKind,
3774
- };
3775
- }
3776
- if (sourceKind !== 'theme-passive-asset' || kind !== 'asset') fail(code);
3777
- const themeAssetPath = asString(output.themeAssetPath, code);
3778
- const asset = assetsByPath.get(themeAssetPath);
3779
- const themeBytes = themeFiles.get(themeAssetPath);
3780
- if (!asset || !themeBytes || usedThemePaths.has(themeAssetPath))
3781
- fail(code);
3782
- usedThemePaths.add(themeAssetPath);
3783
- if (!outputBytes.equals(themeBytes)) fail(code);
3784
- return {
3785
- artifactPath: path,
3786
- licenseConcluded: asset.license,
3787
- sourceKind,
3788
- themeAssetPath,
3789
- };
3790
- },
3791
- );
3792
- if (
3793
- usedThemePaths.size !== assetsByPath.size ||
3794
- [...assetsByPath.keys()].some((path) => !usedThemePaths.has(path))
3795
- ) {
3796
- fail(code);
3797
- }
3798
- conclusions.sort((left, right) =>
3799
- compareUtf8(
3800
- asString(left.artifactPath, code),
3801
- asString(right.artifactPath, code),
3802
- ),
3803
- );
3804
- return { conclusions, expressions };
3805
- }
3806
-
3807
- /**
3808
- * Validate every build-provenance equality and context-backed evidence row.
3809
- *
3810
- * @param {Record<string, unknown>} provenance provenance record
3811
- * @param {Record<string, unknown>} manifest manifest
3812
- * @param {Record<string, unknown>} lock lock
3813
- * @param {Record<string, unknown>} buildInput build input
3814
- * @param {Record<string, unknown>} context trusted context
3815
- * @param {LockState} lockState lock state
3816
- * @param {SourceState} sourceState source state
3817
- * @param {{binding: Record<string, unknown>, claims: Record<string, unknown>, workflowTriggerCommit: string}} workload workload state
3818
- * @param {{workflowFiles: Record<string, unknown>[], actions: Record<string, unknown>[]}} workflow workflow state
3819
- * @param {{outputs: ArtifactOutput[], buildPolicyDecision: Record<string, unknown>}} artifact artifact state
3820
- * @returns {{provenanceDigest: string, licenseExpressions: string[]}}
3821
- */
3822
- function validateBuildProvenance(
3823
- provenance,
3824
- manifest,
3825
- lock,
3826
- buildInput,
3827
- context,
3828
- lockState,
3829
- sourceState,
3830
- workload,
3831
- workflow,
3832
- artifact,
3833
- ) {
3834
- const code = 'PROVENANCE_INVALID';
3835
- ledger(provenance, PROVENANCE_KEYS, code);
3836
- equal(provenance.schemaId, 'urn:gala:metadata:build-provenance:2.0.0', code);
3837
- equal(provenance.schemaVersion, '2.0.0', code);
3838
- jcsEqual(provenance.requiredOidcClaims, OIDC_CLAIMS, code);
3839
- jcsEqual(provenance.workflowFiles, workflow.workflowFiles, code);
3840
- jcsEqual(provenance.actionPins, workflow.actions, code);
3841
- const workloadContext = asObject(context.workload, code);
3842
- const expectedClaims = asObject(workloadContext.expectedClaims, code);
3843
- const asserted = {
3844
- repository: expectedClaims.repository,
3845
- repositoryId: expectedClaims.repository_id,
3846
- repositoryOwner: expectedClaims.repository_owner,
3847
- repositoryOwnerId: expectedClaims.repository_owner_id,
3848
- ref: expectedClaims.ref,
3849
- sourceCommit: buildInput.sourceRevision,
3850
- workflowTriggerCommit: workload.workflowTriggerCommit,
3851
- runId: expectedClaims.run_id,
3852
- runNumber: expectedClaims.run_number,
3853
- runAttempt: Number(asString(expectedClaims.run_attempt, code)),
3854
- eventName: expectedClaims.event_name,
3855
- actor: expectedClaims.actor,
3856
- actorId: expectedClaims.actor_id,
3857
- callerPath: '.github/workflows/gala-publish-v2.yml',
3858
- verificationState: 'pending-authorize-oidc',
3859
- };
3860
- jcsEqual(provenance.assertedWorkload, asserted, code);
3861
- equal(asserted.repository, sourceState.repositoryContext.coordinate, code);
3862
- jcsEqual(workload.binding.callerWorkflow, workflow.workflowFiles[0], code);
3863
-
3864
- const carriers = validateCarriers(context.carriers, asserted);
3865
- const verifiedCarrier = carriers[0];
3866
- const unfrozenCarrier = carriers[1];
3867
- if (!verifiedCarrier || !unfrozenCarrier) fail(code);
3868
- jcsEqual(
3869
- provenance.verifiedInputHandoff,
3870
- verifiedCarrier,
3871
- 'PROVENANCE_CARRIER_INVALID',
3872
- );
3873
- jcsEqual(
3874
- provenance.unfrozenOutputHandoff,
3875
- unfrozenCarrier,
3876
- 'PROVENANCE_CARRIER_INVALID',
3877
- );
3878
- const sandbox = validateSandbox(provenance, context, buildInput);
3879
-
3880
- const publishers = asArray(lock.publisher, code).map((row) =>
3881
- asObject(row, code),
3882
- );
3883
- const policyDecision = artifact.buildPolicyDecision;
3884
- const themeContract = asObject(context.themeContract, code);
3885
- const repository = asObject(buildInput.repository, code);
3886
- const destinationCapabilities = asObject(
3887
- buildInput.destinationCapabilities,
3888
- code,
3889
- );
3890
- const rebuild = {
3891
- repositoryId: repository.repositoryId,
3892
- sourceCommit: buildInput.sourceRevision,
3893
- sourceTree: sourceState.repositoryContext.sourceTree,
3894
- repositoryRootDigest: repository.rootDigest,
3895
- buildEpoch: buildInput.buildEpoch,
3896
- contractVersion: buildInput.contractVersion,
3897
- builder: packageIdentity(publishers[0] ?? {}, code),
3898
- schemas: packageIdentity(asObject(lock.schemas, code), code),
3899
- template: packageIdentity(asObject(lock.template, code), code),
3900
- theme: packageIdentity(asObject(lock.theme, code), code),
3901
- dependencyLockDigest: lock.lockDigest,
3902
- packageReleaseCatalogDigest: lockState.catalog.catalogDigest,
3903
- buildInputDigest: buildInput.inputDigest,
3904
- baseUrl: buildInput.baseUrl,
3905
- basePath: buildInput.basePath,
3906
- destinationCapabilityDigest: destinationCapabilities.capabilityDigest,
3907
- policyReleaseId: policyDecision.policyReleaseId,
3908
- buildPolicyDecisionDigest: policyDecision.decisionDigest,
3909
- stylingContractDigest: themeContract.stylingContractDigest,
3910
- renderPolicy: sourceState.renderPolicy,
3911
- workflowIdentity: manifest.workflowIdentity,
3912
- };
3913
- jcsEqual(provenance.rebuildRecord, rebuild, code);
3914
- equal(provenance.lockDigest, lock.lockDigest, code);
3915
- equal(
3916
- provenance.packageReleaseCatalogDigest,
3917
- lockState.catalog.catalogDigest,
3918
- code,
3919
- );
3920
- equal(provenance.buildInputDigest, buildInput.inputDigest, code);
3921
- equal(provenance.artifactId, manifest.artifactId, code);
3922
- equal(provenance.artifactDigest, manifest.artifactDigest, code);
3923
- equal(provenance.manifestDigest, manifest.manifestDigest, code);
3924
- equal(provenance.policyReleaseId, policyDecision.policyReleaseId, code);
3925
- equal(
3926
- provenance.buildPolicyDecisionDigest,
3927
- policyDecision.decisionDigest,
3928
- code,
3929
- );
3930
- equal(
3931
- provenance.capabilityDecisionDigest,
3932
- digest(context.capabilityDecisionDigest, code),
3933
- code,
3934
- );
3935
- equal(
3936
- provenance.stylingContractDigest,
3937
- themeContract.stylingContractDigest,
3938
- code,
3939
- );
3940
- jcsEqual(provenance.renderPolicy, sourceState.renderPolicy, code);
3941
- jcsEqual(provenance.sandbox, sandbox, code);
3942
- jcsEqual(provenance.secretInputs, [], code);
3943
- equal(provenance.spdx23JsonSchemaDigest, SPDX_SCHEMA_DIGEST, code);
3944
-
3945
- const licenses = validateArtifactLicenses(artifact.outputs, context);
3946
- jcsEqual(
3947
- provenance.artifactLicenseConclusions,
3948
- licenses.conclusions,
3949
- 'PROVENANCE_LICENSE_INVALID',
3950
- );
3951
- const packageExpressions = [...lockState.catalogByKey.values()]
3952
- .filter((entry) =>
3953
- lockState.identities.has(
3954
- `${asString(entry.package, code)}@${asString(entry.version, code)}`,
3955
- ),
3956
- )
3957
- .map((entry) => asString(entry.licenseExpression, code));
3958
- const licenseExpressions = [...packageExpressions, ...licenses.expressions];
3959
- try {
3960
- validateSpdxCatalogEvidence(licenseExpressions, {
3961
- licenseListVersion: provenance.spdxLicenseListVersion,
3962
- licenseListDigest: provenance.spdxLicenseListDigest,
3963
- catalogEntries: context.spdxCatalogEntries,
3964
- });
3965
- } catch {
3966
- fail('PROVENANCE_LICENSE_INVALID');
3967
- }
3968
- return {
3969
- provenanceDigest: digestProfile('buildProvenance', provenance, code),
3970
- licenseExpressions,
3971
- };
3972
- }
3973
-
3974
- /**
3975
- * @param {string} tagged tagged SHA-256
3976
- * @param {string} code diagnostic
3977
- * @returns {string} untagged hexadecimal digest
3978
- */
3979
- function untagSha256(tagged, code) {
3980
- const value = digest(tagged, code);
3981
- return value.slice('sha256:'.length);
3982
- }
3983
-
3984
- /**
3985
- * @param {number} index one-based index
3986
- * @param {number} width decimal width
3987
- * @returns {string} zero-padded index
3988
- */
3989
- function paddedIndex(index, width) {
3990
- return String(index).padStart(width, '0');
3991
- }
3992
-
3993
- /**
3994
- * Validate the exact deterministic SPDX 2.3 projection.
3995
- *
3996
- * @param {Record<string, unknown>} sbom SPDX document
3997
- * @param {Record<string, unknown>} manifest manifest
3998
- * @param {Record<string, unknown>} provenance provenance
3999
- * @param {Record<string, unknown>} lock lock
4000
- * @param {LockState} lockState lock state
4001
- * @param {ArtifactOutput[]} outputs artifact outputs
4002
- * @param {Record<string, unknown>} context trusted context
4003
- * @param {Uint8Array} sbomBytes exact retained JCS bytes
4004
- * @returns {string} exact SBOM digest
4005
- */
4006
- function validateSbom(
4007
- sbom,
4008
- manifest,
4009
- provenance,
4010
- lock,
4011
- lockState,
4012
- outputs,
4013
- context,
4014
- sbomBytes,
4015
- ) {
4016
- const code = 'SPDX_SBOM_INVALID';
4017
- ledger(
4018
- sbom,
4019
- [
4020
- 'SPDXID',
4021
- 'spdxVersion',
4022
- 'dataLicense',
4023
- 'name',
4024
- 'documentNamespace',
4025
- 'creationInfo',
4026
- 'documentDescribes',
4027
- 'packages',
4028
- 'files',
4029
- 'relationships',
4030
- ],
4031
- code,
4032
- );
4033
- const artifactHex = untagSha256(
4034
- asString(manifest.artifactDigest, code),
4035
- code,
4036
- );
4037
- const name = `galascribe-artifact-${artifactHex}`;
4038
- const publishers = asArray(lock.publisher, code).map((row) =>
4039
- asObject(row, code),
4040
- );
4041
- const publishAction = publishers[0];
4042
- if (!publishAction) fail(code);
4043
- const creationInfo = {
4044
- created: manifest.generatedAt,
4045
- creators: [
4046
- `Tool: @rathnasgala2/publish-action-${asString(publishAction.version, code)}`,
4047
- ],
4048
- licenseListVersion: provenance.spdxLicenseListVersion,
4049
- };
4050
-
4051
- const packageIds = new Map();
4052
- const directRows = [
4053
- asObject(lock.schemas, code),
4054
- asObject(lock.template, code),
4055
- asObject(lock.theme, code),
4056
- ...publishers,
4057
- ];
4058
- const dependencyRows = asArray(lock.dependencies, code).map((row) =>
4059
- asObject(row, code),
4060
- );
4061
- const artifactPackage = {
4062
- SPDXID: 'SPDXRef-Package-Artifact',
4063
- name,
4064
- versionInfo: manifest.sourceCommit,
4065
- downloadLocation: 'NOASSERTION',
4066
- filesAnalyzed: true,
4067
- packageVerificationCode: { packageVerificationCodeValue: '' },
4068
- licenseConcluded: 'NOASSERTION',
4069
- licenseDeclared: 'NOASSERTION',
4070
- copyrightText: 'NOASSERTION',
4071
- };
4072
- /** @type {Record<string, unknown>[]} */
4073
- const expectedPackages = [artifactPackage];
4074
- for (let index = 0; index < directRows.length; index += 1) {
4075
- const row = directRows[index];
4076
- if (!row) fail(code);
4077
- const id = `SPDXRef-Package-Direct-${paddedIndex(index, 2)}`;
4078
- packageIds.set(packageKey(packageIdentity(row, code)), id);
4079
- expectedPackages.push(spdxDependencyPackage(row, id, lockState, code));
4080
- }
4081
- for (let index = 0; index < dependencyRows.length; index += 1) {
4082
- const row = dependencyRows[index];
4083
- if (!row) fail(code);
4084
- const id = `SPDXRef-Package-Dependency-${paddedIndex(index + 1, 6)}`;
4085
- packageIds.set(packageKey(packageIdentity(row, code)), id);
4086
- expectedPackages.push(spdxDependencyPackage(row, id, lockState, code));
4087
- }
4088
-
4089
- const conclusionByPath = new Map(
4090
- asArray(provenance.artifactLicenseConclusions, code).map((candidate) => {
4091
- const row = asObject(candidate, code);
4092
- return [
4093
- asString(row.artifactPath, code),
4094
- asString(row.licenseConcluded, code),
4095
- ];
4096
- }),
4097
- );
4098
- const sortedOutputs = [...outputs].sort((left, right) =>
4099
- compareUtf8(left.path, right.path),
4100
- );
4101
- const expectedFiles = sortedOutputs.map((output, index) => ({
4102
- SPDXID: `SPDXRef-File-${paddedIndex(index + 1, 6)}`,
4103
- fileName: `./${output.path}`,
4104
- checksums: [
4105
- { algorithm: 'SHA1', checksumValue: rawHash(output.bytes, 'sha1') },
4106
- { algorithm: 'SHA256', checksumValue: rawHash(output.bytes, 'sha256') },
4107
- ],
4108
- licenseConcluded: conclusionByPath.get(output.path),
4109
- copyrightText: 'NOASSERTION',
4110
- }));
4111
- if (expectedFiles.some((file) => typeof file.licenseConcluded !== 'string'))
4112
- fail(code);
4113
- const sortedFileSha1 = expectedFiles
4114
- .map((file) => file.checksums[0]?.checksumValue ?? '')
4115
- .sort(compareUtf8)
4116
- .join('');
4117
- artifactPackage.packageVerificationCode.packageVerificationCodeValue =
4118
- rawHash(Buffer.from(sortedFileSha1, 'ascii'), 'sha1');
4119
-
4120
- const relationships = [
4121
- {
4122
- spdxElementId: 'SPDXRef-DOCUMENT',
4123
- relationshipType: 'DESCRIBES',
4124
- relatedSpdxElement: 'SPDXRef-Package-Artifact',
4125
- },
4126
- ...expectedFiles.map((file) => ({
4127
- spdxElementId: 'SPDXRef-Package-Artifact',
4128
- relationshipType: 'CONTAINS',
4129
- relatedSpdxElement: file.SPDXID,
4130
- })),
4131
- ...directRows.map((row) => ({
4132
- spdxElementId: 'SPDXRef-Package-Artifact',
4133
- relationshipType: 'DEPENDS_ON',
4134
- relatedSpdxElement: packageIds.get(
4135
- packageKey(packageIdentity(row, code)),
4136
- ),
4137
- })),
4138
- ...asArray(lock.dependencyDag, code).map((candidate) => {
4139
- const edge = asObject(candidate, code);
4140
- const from = packageIds.get(asString(edge.from, code));
4141
- const to = packageIds.get(asString(edge.to, code));
4142
- if (!from || !to) fail(code);
4143
- return {
4144
- spdxElementId: from,
4145
- relationshipType: 'DEPENDS_ON',
4146
- relatedSpdxElement: to,
4147
- };
4148
- }),
4149
- ];
4150
- const relationshipMap = new Map(
4151
- relationships.map((relationship) => [
4152
- canonicalizeJcs(relationship),
4153
- relationship,
4154
- ]),
4155
- );
4156
- const expectedRelationships = [...relationshipMap.values()].sort(
4157
- (left, right) => {
4158
- const element = compareUtf8(
4159
- asString(left.spdxElementId, code),
4160
- asString(right.spdxElementId, code),
4161
- );
4162
- if (element) return element;
4163
- const type = compareUtf8(
4164
- asString(left.relationshipType, code),
4165
- asString(right.relationshipType, code),
4166
- );
4167
- return (
4168
- type ||
4169
- compareUtf8(
4170
- asString(left.relatedSpdxElement, code),
4171
- asString(right.relatedSpdxElement, code),
4172
- )
4173
- );
4174
- },
4175
- );
4176
- const expected = {
4177
- SPDXID: 'SPDXRef-DOCUMENT',
4178
- spdxVersion: 'SPDX-2.3',
4179
- dataLicense: 'CC0-1.0',
4180
- name,
4181
- documentNamespace: `urn:gala:spdx:2:${artifactHex}`,
4182
- creationInfo,
4183
- documentDescribes: ['SPDXRef-Package-Artifact'],
4184
- packages: expectedPackages,
4185
- files: expectedFiles,
4186
- relationships: expectedRelationships,
4187
- };
4188
- jcsEqual(sbom, expected, code);
4189
- const retainedBytes = bytes(sbomBytes, code);
4190
- if (
4191
- retainedBytes.length > 16_777_216 ||
4192
- !retainedBytes.equals(canonicalizeJcsBytes(sbom))
4193
- ) {
4194
- fail(code);
4195
- }
4196
- const official = ledger(
4197
- context.officialSpdxValidation,
4198
- ['schemaDigest', 'accepted'],
4199
- code,
4200
- );
4201
- equal(official.schemaDigest, SPDX_SCHEMA_DIGEST, code);
4202
- equal(official.accepted, true, code);
4203
- const sbomDigest = sha256Tagged(retainedBytes);
4204
- equal(provenance.sbomDigest, sbomDigest, code);
4205
- return sbomDigest;
4206
- }
4207
-
4208
- /**
4209
- * Build one exact SPDX dependency-package row.
4210
- *
4211
- * @param {Record<string, unknown>} row locked package row
4212
- * @param {string} id assigned SPDX ID
4213
- * @param {LockState} lockState package state
4214
- * @param {string} code diagnostic
4215
- * @returns {Record<string, unknown>} SPDX package
4216
- */
4217
- function spdxDependencyPackage(row, id, lockState, code) {
4218
- const identity = packageIdentity(row, code);
4219
- const catalog = lockState.catalogByKey.get(packageKey(identity));
4220
- if (!catalog) fail(code);
4221
- return {
4222
- SPDXID: id,
4223
- name: identity.package,
4224
- versionInfo: identity.version,
4225
- downloadLocation: 'NOASSERTION',
4226
- filesAnalyzed: false,
4227
- checksums: [
4228
- {
4229
- algorithm: 'SHA256',
4230
- checksumValue: untagSha256(identity.integrity, code),
4231
- },
4232
- ],
4233
- licenseConcluded: catalog.licenseExpression,
4234
- licenseDeclared: catalog.licenseExpression,
4235
- copyrightText: 'NOASSERTION',
4236
- };
4237
- }
4238
-
4239
- /**
4240
- * Validate the complete accepted S0-T03 lock/build/artifact record family.
4241
- *
4242
- * Structural JSON Schema validation is a required predecessor. This function
4243
- * then enforces DEC-097 cross-schema invariants 1, 2, 5, 6, and 7 against
4244
- * exact retained context bytes without adding context facts to wire records.
4245
- *
4246
- * @param {unknown} recordBundle exact record bundle
4247
- * @param {unknown} validatorContext trusted validation context
4248
- * @returns {{
4249
- * lockDigest: string,
4250
- * buildInputDigest: string,
4251
- * artifactDigest: string,
4252
- * sourceInventoryDigest: string,
4253
- * validationEvidenceDigest: string,
4254
- * manifestDigest: string,
4255
- * buildPolicyDecisionDigest: string,
4256
- * provenanceDigest: string,
4257
- * sbomDigest: string,
4258
- * workloadBindingDigest: string,
4259
- * artifactFileCount: number
4260
- * }} independently recomputed semantic identities
4261
- */
4262
- export function validateBuildArtifactSemantics(recordBundle, validatorContext) {
4263
- const records = ledger(
4264
- recordBundle,
4265
- ['lock', 'buildInput', 'artifactManifest', 'buildProvenance', 'sbom'],
4266
- 'BUILD_ARTIFACT_RECORD_BUNDLE_INVALID',
4267
- );
4268
- const context = ledger(
4269
- validatorContext,
4270
- [
4271
- 'packageReleaseCatalog',
4272
- 'registryFetchProfile',
4273
- 'packageArtifacts',
4274
- 'themeContract',
4275
- 'adapterCapability',
4276
- 'repository',
4277
- 'normalizedSource',
4278
- 'renderPolicyBytes',
4279
- 'frozenEnvelopeBytes',
4280
- 'artifactOutputs',
4281
- 'reproducibilityRuns',
4282
- 'buildPolicyRelease',
4283
- 'buildPolicyDecision',
4284
- 'workflowFiles',
4285
- 'actions',
4286
- 'workload',
4287
- 'carriers',
4288
- 'runner',
4289
- 'themeFiles',
4290
- 'capabilityDecisionDigest',
4291
- 'spdxCatalogEntries',
4292
- 'officialSpdxValidation',
4293
- ],
4294
- 'BUILD_ARTIFACT_CONTEXT_INVALID',
4295
- );
4296
- const lock = asObject(records.lock, 'LOCK_LEDGER_INVALID');
4297
- const buildInput = asObject(records.buildInput, 'BUILD_INPUT_INVALID');
4298
- const manifest = asObject(
4299
- records.artifactManifest,
4300
- 'ARTIFACT_MANIFEST_INVALID',
4301
- );
4302
- const provenance = asObject(records.buildProvenance, 'PROVENANCE_INVALID');
4303
- const sbom = asObject(records.sbom, 'SPDX_SBOM_INVALID');
4304
- const frozen = validateFrozen(records, context);
4305
- const lockState = validateLock(lock, buildInput, context);
4306
- equal(
4307
- lock.lockDigest,
4308
- digestProfile('lock', lock, 'LOCK_DIGEST_INVALID'),
4309
- 'LOCK_DIGEST_INVALID',
4310
- );
4311
- const sourceState = validateBuildInput(lock, buildInput, context);
4312
- validateContractProjections(lock, buildInput, context, lockState);
4313
- ledger(provenance, PROVENANCE_KEYS, 'PROVENANCE_INVALID');
4314
- const workflow = validateWorkflowEvidence(provenance, context, sourceState);
4315
- const workload = validateWorkload(
4316
- asObject(context.workload, 'WORKLOAD_INVALID'),
4317
- workflow.workflowFiles,
4318
- sourceState,
4319
- );
4320
- const artifact = validateArtifactManifest(
4321
- manifest,
4322
- lock,
4323
- buildInput,
4324
- provenance,
4325
- context,
4326
- lockState,
4327
- sourceState,
4328
- workload,
4329
- workflow.workflowFiles,
4330
- frozen.payloads,
4331
- );
4332
- const provenanceState = validateBuildProvenance(
4333
- provenance,
4334
- manifest,
4335
- lock,
4336
- buildInput,
4337
- context,
4338
- lockState,
4339
- sourceState,
4340
- workload,
4341
- workflow,
4342
- artifact,
4343
- );
4344
- const sbomRecord = frozen.envelope.records.find(
4345
- (record) => record.kind === 0x04,
4346
- );
4347
- if (!sbomRecord) fail('SPDX_SBOM_INVALID');
4348
- const sbomDigest = validateSbom(
4349
- sbom,
4350
- manifest,
4351
- provenance,
4352
- lock,
4353
- lockState,
4354
- artifact.outputs,
4355
- context,
4356
- sbomRecord.content,
4357
- );
4358
- validateFrozenRecordParity(records, frozen.envelope);
4359
- return {
4360
- lockDigest: asString(lock.lockDigest, 'LOCK_DIGEST_INVALID'),
4361
- buildInputDigest: asString(buildInput.inputDigest, 'BUILD_DIGEST_INVALID'),
4362
- artifactDigest: asString(
4363
- manifest.artifactDigest,
4364
- 'ARTIFACT_DIGEST_INVALID',
4365
- ),
4366
- sourceInventoryDigest: asString(
4367
- manifest.sourceInventoryDigest,
4368
- 'ARTIFACT_SOURCE_PARTITION_INVALID',
4369
- ),
4370
- validationEvidenceDigest: asString(
4371
- asObject(manifest.validation, 'ARTIFACT_VALIDATION_INVALID')
4372
- .evidenceDigest,
4373
- 'ARTIFACT_VALIDATION_INVALID',
4374
- ),
4375
- manifestDigest: asString(
4376
- manifest.manifestDigest,
4377
- 'ARTIFACT_DIGEST_INVALID',
4378
- ),
4379
- buildPolicyDecisionDigest: asString(
4380
- artifact.buildPolicyDecision.decisionDigest,
4381
- 'ARTIFACT_POLICY_INVALID',
4382
- ),
4383
- provenanceDigest: provenanceState.provenanceDigest,
4384
- sbomDigest,
4385
- workloadBindingDigest: asString(
4386
- workload.binding.workloadBindingDigest,
4387
- 'WORKLOAD_INVALID',
4388
- ),
4389
- artifactFileCount: artifact.outputs.length,
4390
- };
4391
- }