@rathnasgala2/schemas 2.11.0 → 2.12.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.
- package/README.md +164 -56
- package/THIRD_PARTY_LICENSES.json +79 -106
- package/diagnostics/diagnostic-map.json +443 -405
- package/diagnostics/diagnostic-map.public-runtime-origins.json +5 -0
- package/docs/catalogs/schema-inventory.json +33 -54
- package/examples/valid/adapter-capability/rules.json +11 -0
- package/examples/valid/artifact-manifest/rules.json +156 -177
- package/examples/valid/build-input/rules.json +67 -63
- package/examples/valid/build-provenance/rules.json +151 -172
- package/examples/valid/deployment-intent/rules.json +152 -148
- package/examples/valid/deployment-observation/rules.json +24 -20
- package/examples/valid/deployment-receipt/rules.json +152 -148
- package/examples/valid/navigation/rules.json +2 -4
- package/examples/valid/theme-contract/rules.json +4 -1
- package/fixtures/boundary/adapter-capability/rules.json +16 -0
- package/fixtures/boundary/artifact-manifest/rules.json +195 -202
- package/fixtures/boundary/build-input/rules.json +99 -86
- package/fixtures/boundary/build-provenance/rules.json +193 -202
- package/fixtures/boundary/deployment-intent/rules.json +186 -140
- package/fixtures/boundary/deployment-observation/rules.json +68 -18
- package/fixtures/boundary/deployment-receipt/rules.json +186 -140
- package/fixtures/boundary/navigation/rules.json +2 -17
- package/fixtures/boundary/theme-contract/rules.json +34 -0
- package/fixtures/invalid/adapter-capability/SCHEMA_STRING_TOO_LONG/cases.json +11 -0
- package/fixtures/invalid/adapter-capability/UTF8_BYTE_LENGTH_INVALID/cases.json +11 -0
- package/fixtures/invalid/artifact-manifest/ASCII_BYTE_LENGTH_INVALID/cases.json +13 -0
- package/fixtures/invalid/artifact-manifest/INTEGER_RANGE_INVALID/cases.json +24 -0
- package/fixtures/invalid/artifact-manifest/REQUEST_FIELD_UNKNOWN/cases.json +19 -19
- package/fixtures/invalid/artifact-manifest/SCHEMA_CONSTANT_INVALID/cases.json +48 -48
- package/fixtures/invalid/artifact-manifest/SCHEMA_ENUM_INVALID/cases.json +11 -11
- package/fixtures/invalid/artifact-manifest/SCHEMA_FORMAT_INVALID/cases.json +0 -9
- package/fixtures/invalid/artifact-manifest/SCHEMA_NEGATED_RULE_MATCHED/cases.json +0 -11
- package/fixtures/invalid/artifact-manifest/SCHEMA_PATTERN_INVALID/cases.json +0 -22
- package/fixtures/invalid/artifact-manifest/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +1105 -1105
- package/fixtures/invalid/artifact-manifest/SCHEMA_STRING_TOO_LONG/cases.json +4 -6
- package/fixtures/invalid/artifact-manifest/SCHEMA_STRING_TOO_SHORT/cases.json +9 -11
- package/fixtures/invalid/artifact-manifest/SCHEMA_TYPE_INVALID/cases.json +22 -31
- package/fixtures/invalid/artifact-manifest/UTF8_BYTE_LENGTH_INVALID/cases.json +0 -11
- package/fixtures/invalid/build-input/INTEGER_RANGE_INVALID/cases.json +13 -0
- package/fixtures/invalid/build-input/REQUEST_FIELD_UNKNOWN/cases.json +32 -32
- package/fixtures/invalid/build-input/SCHEMA_CONSTANT_INVALID/cases.json +11 -11
- package/fixtures/invalid/build-input/SCHEMA_ENUM_INVALID/cases.json +11 -11
- package/fixtures/invalid/build-input/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +78 -78
- package/fixtures/invalid/build-input/SCHEMA_TYPE_INVALID/cases.json +51 -18
- package/fixtures/invalid/build-provenance/ASCII_BYTE_LENGTH_INVALID/cases.json +13 -0
- package/fixtures/invalid/build-provenance/INTEGER_RANGE_INVALID/cases.json +22 -0
- package/fixtures/invalid/build-provenance/REQUEST_FIELD_UNKNOWN/cases.json +56 -56
- package/fixtures/invalid/build-provenance/SCHEMA_CONSTANT_INVALID/cases.json +37 -37
- package/fixtures/invalid/build-provenance/SCHEMA_ENUM_INVALID/cases.json +11 -11
- package/fixtures/invalid/build-provenance/SCHEMA_FORMAT_INVALID/cases.json +0 -9
- package/fixtures/invalid/build-provenance/SCHEMA_NEGATED_RULE_MATCHED/cases.json +0 -11
- package/fixtures/invalid/build-provenance/SCHEMA_PATTERN_INVALID/cases.json +0 -22
- package/fixtures/invalid/build-provenance/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +927 -927
- package/fixtures/invalid/build-provenance/SCHEMA_STRING_TOO_LONG/cases.json +9 -11
- package/fixtures/invalid/build-provenance/SCHEMA_STRING_TOO_SHORT/cases.json +8 -10
- package/fixtures/invalid/build-provenance/SCHEMA_TYPE_INVALID/cases.json +20 -25
- package/fixtures/invalid/build-provenance/UTF8_BYTE_LENGTH_INVALID/cases.json +0 -11
- package/fixtures/invalid/deployment-intent/INTEGER_RANGE_INVALID/cases.json +24 -0
- package/fixtures/invalid/deployment-intent/REQUEST_FIELD_UNKNOWN/cases.json +30 -30
- package/fixtures/invalid/deployment-intent/SCHEMA_CONSTANT_INVALID/cases.json +41 -41
- package/fixtures/invalid/deployment-intent/SCHEMA_ENUM_INVALID/cases.json +11 -11
- package/fixtures/invalid/deployment-intent/SCHEMA_FORMAT_INVALID/cases.json +0 -9
- package/fixtures/invalid/deployment-intent/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +1209 -1209
- package/fixtures/invalid/deployment-intent/SCHEMA_STRING_TOO_LONG/cases.json +9 -0
- package/fixtures/invalid/deployment-intent/SCHEMA_STRING_TOO_SHORT/cases.json +9 -0
- package/fixtures/invalid/deployment-intent/SCHEMA_TYPE_INVALID/cases.json +22 -20
- package/fixtures/invalid/deployment-observation/INTEGER_RANGE_INVALID/cases.json +24 -0
- package/fixtures/invalid/deployment-observation/REQUEST_FIELD_UNKNOWN/cases.json +16 -16
- package/fixtures/invalid/deployment-observation/SCHEMA_FORMAT_INVALID/cases.json +0 -9
- package/fixtures/invalid/deployment-observation/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +39 -39
- package/fixtures/invalid/deployment-observation/SCHEMA_STRING_TOO_LONG/cases.json +11 -0
- package/fixtures/invalid/deployment-observation/SCHEMA_STRING_TOO_SHORT/cases.json +11 -0
- package/fixtures/invalid/deployment-observation/SCHEMA_TYPE_INVALID/cases.json +11 -11
- package/fixtures/invalid/deployment-receipt/INTEGER_RANGE_INVALID/cases.json +24 -0
- package/fixtures/invalid/deployment-receipt/REQUEST_FIELD_UNKNOWN/cases.json +70 -70
- package/fixtures/invalid/deployment-receipt/SCHEMA_CONSTANT_INVALID/cases.json +36 -36
- package/fixtures/invalid/deployment-receipt/SCHEMA_ENUM_INVALID/cases.json +11 -11
- package/fixtures/invalid/deployment-receipt/SCHEMA_FORMAT_INVALID/cases.json +0 -9
- package/fixtures/invalid/deployment-receipt/SCHEMA_REQUIRED_FIELD_MISSING/cases.json +1304 -1304
- package/fixtures/invalid/deployment-receipt/SCHEMA_STRING_TOO_LONG/cases.json +9 -0
- package/fixtures/invalid/deployment-receipt/SCHEMA_STRING_TOO_SHORT/cases.json +9 -0
- package/fixtures/invalid/deployment-receipt/SCHEMA_TYPE_INVALID/cases.json +22 -20
- package/fixtures/invalid/navigation/SCHEMA_ARRAY_TOO_LONG/cases.json +0 -15
- package/fixtures/invalid/navigation/SCHEMA_CONSTANT_INVALID/cases.json +11 -0
- package/fixtures/invalid/navigation/SCHEMA_TYPE_INVALID/cases.json +0 -11
- package/fixtures/invalid/theme-contract/INTEGER_RANGE_INVALID/cases.json +11 -0
- package/fixtures/invalid/theme-contract/SCHEMA_FORMAT_INVALID/cases.json +0 -9
- package/fixtures/invalid/theme-contract/SCHEMA_STRING_TOO_LONG/cases.json +9 -0
- package/fixtures/invalid/theme-contract/SCHEMA_STRING_TOO_SHORT/cases.json +9 -0
- package/fixtures/manifest.json +3217 -2930
- package/fixtures/unknown-field/adapter-capability/rules.json +2 -0
- package/fixtures/unknown-field/artifact-manifest/rules.json +111 -114
- package/fixtures/unknown-field/build-input/rules.json +36 -32
- package/fixtures/unknown-field/build-provenance/rules.json +111 -114
- package/fixtures/unknown-field/deployment-intent/rules.json +109 -105
- package/fixtures/unknown-field/deployment-observation/rules.json +24 -20
- package/fixtures/unknown-field/deployment-receipt/rules.json +109 -105
- package/fixtures/unknown-field/navigation/rules.json +1 -3
- package/fixtures/unknown-field/theme-contract/rules.json +3 -0
- package/generated/browser/runtime-origins-validator-core.d.mts +10 -0
- package/generated/browser/runtime-origins-validator-core.mjs +35 -0
- package/generated/browser/validator-core.d.mts +10 -0
- package/generated/browser/validator-core.mjs +80 -0
- package/generated/typescript/contracts/artifact-manifest.d.ts +49 -48
- package/generated/typescript/contracts/build-input.d.ts +15 -15
- package/generated/typescript/contracts/build-provenance.d.ts +51 -50
- package/generated/typescript/contracts/deployment-intent.d.ts +6 -6
- package/generated/typescript/contracts/deployment-observation.d.ts +1 -1
- package/generated/typescript/contracts/deployment-receipt.d.ts +5 -5
- package/generated/typescript/contracts/navigation.d.ts +1 -1
- package/generated/typescript/index.js +11 -37
- package/openapi/http-catalog.json +2 -2
- package/openapi/openapi.yaml +18 -10
- package/package.json +19 -16
- package/sbom.cdx.json +23 -23
- package/schemas/adapter-capability.schema.json +5 -1
- package/schemas/appearance.schema.json +1 -1
- package/schemas/artifact-manifest.schema.json +15 -18
- package/schemas/build-input.schema.json +18 -11
- package/schemas/build-provenance.schema.json +15 -18
- package/schemas/deployment-intent.schema.json +13 -8
- package/schemas/deployment-observation.schema.json +10 -5
- package/schemas/deployment-receipt.schema.json +12 -7
- package/schemas/navigation.schema.json +5 -26
- package/schemas/theme-contract.schema.json +4 -2
- package/src/index.js +2 -2
- package/src/internal/browser-schema-validator.js +43 -0
- package/src/internal/format-validators.js +1 -1
- package/src/internal/gala-keywords.js +148 -0
- package/src/internal/validator-core.js +244 -108
- package/src/runtime-origins.js +18 -18
- package/types/index.d.ts +2 -2
- package/types/internal/browser-schema-validator.d.ts +16 -0
- package/types/internal/format-validators.d.ts +1 -1
- package/types/internal/gala-keywords.d.ts +68 -0
- package/types/internal/validator-core.d.ts +80 -1
- package/compatibility/compatibility.json +0 -594
- package/generated/java/src/main/java/io/gala/schema/generated/GalaSchemaRegistry.java +0 -67
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityAdapterConfigurationCapabilities.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityAdapterIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityCapabilityDecision.java +0 -62
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityCredentialEgressProfile.java +0 -35
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityDestinationIdentity.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityDestinationProviderCoordinates.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityDocument.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityFilesystemProviderLimits.java +0 -32
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityGithubActionsOidcOriginCatalog.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityHttpProviderLimits.java +0 -72
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemAllowlist.java +0 -21
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemAllowlistEntry.java +0 -28
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemCapabilityEvidence.java +0 -42
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemControlPublication.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemControlRow.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemFailureEvidence.java +0 -40
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemObservationEvidence.java +0 -40
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemPlatform.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemProbeSummary.java +0 -32
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemProbeTranscript.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemReplacement.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityLocalFilesystemSurfaceIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityManagedExecutionBudget.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityPagesOidcCredentialEgress.java +0 -30
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderCallClassBinding.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderCredentialEgress.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderCredentialHeader.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderDerivedHeader.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderFixedHeader.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilityProviderRequestTemplate.java +0 -39
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneBinding.java +0 -32
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneRequest.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneRequestCatalog.java +0 -31
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesControlPlaneResponseCatalog.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/AdapterCapabilitySpacesWebsiteConfiguration.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceColorMode.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceDocument.java +0 -37
- package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceExtensionMap.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/AppearanceSemanticTokens.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestActionPinEvidence.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestArtifactLicenseConclusion.java +0 -21
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestAssertedWorkload.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestBuildSandboxEvidence.java +0 -38
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestDocument.java +0 -73
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestFinding.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestAsset.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestCompositionIdentity.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestExcludedInput.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestIncludedSource.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestRedirect.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestRoute.java +0 -28
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestSourceIdentity.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestManifestValidationEvidence.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestMeasurement.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestPackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestRenderPolicyIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestReproducibleBuildRecord.java +0 -56
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestWorkflowCarrierEvidence.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/ArtifactManifestWorkflowFileEvidence.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorDocument.java +0 -33
- package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorExtensionMap.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorLocalizedAuthor.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/AuthorSocialLink.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputAdapterIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputAppearanceNormalized.java +0 -33
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputAuthorNormalized.java +0 -31
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputBuildPackages.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputColorMode.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputContentBuildRecord.java +0 -33
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputContentFrontmatterNormalized.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputDestinationCapabilityProfile.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputDocument.java +0 -51
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputLocalizedAuthor.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputModuleBuildSelection.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputNavigationItem.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputNavigationLeaf.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputNavigationNormalized.java +0 -21
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputPackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputPublicationNormalized.java +0 -39
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputRenderPolicyIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputRenderableBody.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputRepositorySnapshot.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputResolvedFile.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputResolvedMedia.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputSemanticTokens.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildInputSocialLink.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceActionPinEvidence.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceArtifactLicenseConclusion.java +0 -21
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceAssertedWorkload.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceBuildSandboxEvidence.java +0 -38
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceDocument.java +0 -69
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceFinding.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestAsset.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestCompositionIdentity.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestExcludedInput.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestIncludedSource.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestRedirect.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestRoute.java +0 -28
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestSourceIdentity.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceManifestValidationEvidence.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceMeasurement.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenancePackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceRenderPolicyIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceReproducibleBuildRecord.java +0 -56
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceWorkflowCarrierEvidence.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/BuildProvenanceWorkflowFileEvidence.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/ContentFrontmatterDocument.java +0 -50
- package/generated/java/src/main/java/io/gala/schema/generated/model/ContentFrontmatterExtensionMap.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/ContentFrontmatterMediaRef.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationBasis.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationDetectionObservation.java +0 -33
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationDetectionPlan.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentActivationDetectionProbe.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentAdapterIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentCancellationFinalizationEvidence.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDeadlineFinalizationEvidence.java +0 -39
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDeadlineFinalizationStream.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDeploymentPolicyDecision.java +0 -75
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDestinationIdentity.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDestinationMutationAuthority.java +0 -36
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDestinationProviderCoordinates.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentDocument.java +0 -136
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentExpectedRedirectHop.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentObservedVerificationHeader.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesInterveningRunAttempt.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesReconciliationCommand.java +0 -48
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesReconciliationRecovery.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPagesRunAttemptGapProof.java +0 -27
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPublicGenerationMarkerPayload.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentPublicProbeObservation.java +0 -54
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentRecognizedPriorRouteContract.java +0 -30
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentRenderPolicyIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentReproducibleBuildRecord.java +0 -56
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentSupersessionFinalizationEvidence.java +0 -45
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentVerificationHeaderExpectation.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentIntentVerificationPlanTarget.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationActivationDetectionObservation.java +0 -33
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationActivationDetectionProbe.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationAdapterIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationCancellationFinalizationEvidence.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDeadlineFinalizationEvidence.java +0 -39
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDeadlineFinalizationStream.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDestinationIdentity.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDestinationProviderCoordinates.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationDocument.java +0 -57
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationExpectedRedirectHop.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationObservedVerificationHeader.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationPackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationPublicProbeObservation.java +0 -54
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationRecognizedPriorRouteContract.java +0 -30
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationRenderPolicyIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationSupersessionFinalizationEvidence.java +0 -45
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationVerificationHeaderExpectation.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentObservationVerificationPlanTarget.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationBasis.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationDetectionObservation.java +0 -33
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationDetectionPlan.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptActivationDetectionProbe.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptAdapterIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptCancellationFinalizationEvidence.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeadlineFinalizationEvidence.java +0 -39
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeadlineFinalizationStream.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentAttempt.java +0 -38
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentFailure.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentObservation.java +0 -53
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentPolicyDecision.java +0 -75
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageEvidence.java +0 -36
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageInput.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageObservationEvidenceReference.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDeploymentStageResult.java +0 -31
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDestinationIdentity.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDestinationMutationAuthority.java +0 -36
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDestinationProviderCoordinates.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptDocument.java +0 -105
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptExpectedRedirectHop.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptFinding.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptObservedVerificationHeader.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesInterveningRunAttempt.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesReconciliationCommand.java +0 -48
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesReconciliationRecovery.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPagesRunAttemptGapProof.java +0 -27
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPublicGenerationMarkerPayload.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptPublicProbeObservation.java +0 -54
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptRecognizedPriorRouteContract.java +0 -30
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptRenderPolicyIdentity.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptReproducibleBuildRecord.java +0 -56
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptSupersessionFinalizationEvidence.java +0 -45
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptVerificationHeaderExpectation.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/DeploymentReceiptVerificationPlanTarget.java +0 -52
- package/generated/java/src/main/java/io/gala/schema/generated/model/EventEnvelopeDocument.java +0 -46
- package/generated/java/src/main/java/io/gala/schema/generated/model/EventEnvelopeRegisteredEventPayload.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/LockDependencyEdge.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/LockDocument.java +0 -39
- package/generated/java/src/main/java/io/gala/schema/generated/model/LockLockedPackage.java +0 -26
- package/generated/java/src/main/java/io/gala/schema/generated/model/LockLockedTemplate.java +0 -28
- package/generated/java/src/main/java/io/gala/schema/generated/model/LockPackageIdentity.java +0 -22
- package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationDocument.java +0 -25
- package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationExtensionMap.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationNavigationItem.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/NavigationNavigationLeaf.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/ProblemDocument.java +0 -37
- package/generated/java/src/main/java/io/gala/schema/generated/model/ProblemFieldError.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicGenerationMarkerDocument.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicRuntimeOriginsDocument.java +0 -44
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationDocument.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationExtensionMap.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationFooterCard.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationPublicationProfile.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/PublicationSocialLink.java +0 -19
- package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryAssetRoot.java +0 -16
- package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryContentRoot.java +0 -23
- package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryDocument.java +0 -41
- package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryExtensionMap.java +0 -15
- package/generated/java/src/main/java/io/gala/schema/generated/model/RepositoryGeneratedRoot.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/TemplateCompositionDocument.java +0 -18
- package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractDocument.java +0 -53
- package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractPassiveAsset.java +0 -24
- package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractThemeBudgets.java +0 -20
- package/generated/java/src/main/java/io/gala/schema/generated/model/ThemeContractThemeToken.java +0 -22
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/adapter-capability.schema.json +0 -2689
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/appearance.schema.json +0 -226
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/artifact-manifest.schema.json +0 -1063
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/author.schema.json +0 -231
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/build-input.schema.json +0 -866
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/build-provenance.schema.json +0 -937
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/content-frontmatter.schema.json +0 -241
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/deployment-intent.schema.json +0 -2067
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/deployment-observation.schema.json +0 -1654
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/deployment-receipt.schema.json +0 -5546
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/event-envelope.schema.json +0 -195
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/lock.schema.json +0 -326
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/navigation.schema.json +0 -229
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/problem.schema.json +0 -194
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/public-generation-marker.schema.json +0 -142
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/public-runtime-origins.schema.json +0 -183
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/publication.schema.json +0 -248
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/repository.schema.json +0 -219
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/template-composition.schema.json +0 -133
- package/generated/java/src/main/resources/io/gala/schema/generated/schemas/theme-contract.schema.json +0 -850
- package/generated/typescript/validator-core.cjs +0 -17
- package/openapi/generator/spring.json +0 -22
- package/openapi/generator/typescript-fetch.json +0 -19
- package/openapi/source/authentication.yaml +0 -557
- package/openapi/source/callbacks.yaml +0 -332
- package/openapi/source/components.yaml +0 -5670
- package/openapi/source/github.yaml +0 -351
- package/openapi/source/internal.yaml +0 -71
- package/openapi/source/membership-invitations.yaml +0 -203
- package/openapi/source/organizations.yaml +0 -11144
- package/openapi/source/root.yaml +0 -20
- package/openapi/source/self.yaml +0 -1940
- package/openapi/source/session.yaml +0 -308
- package/openapi/source/workloads.yaml +0 -744
- package/src/internal/build-artifact-semantics.js +0 -4391
- package/src/internal/deployment-record-semantics.js +0 -7567
- package/src/internal/deployment-stage-semantics.js +0 -1207
- package/src/internal/http-problem-contract.js +0 -35
- package/src/internal/managed-evidence-journal.js +0 -160
- package/src/internal/public-runtime-origins.js +0 -143
- package/src/internal/theme-composition-semantics.js +0 -2441
- package/types/internal/build-artifact-semantics.d.ts +0 -77
- package/types/internal/deployment-record-semantics.d.ts +0 -313
- package/types/internal/deployment-stage-semantics.d.ts +0 -108
- package/types/internal/http-problem-contract.d.ts +0 -4
- package/types/internal/managed-evidence-journal.d.ts +0 -26
- package/types/internal/public-runtime-origins.d.ts +0 -22
- 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('&', '&')
|
|
2969
|
-
.replaceAll('<', '<')
|
|
2970
|
-
.replaceAll('>', '>')
|
|
2971
|
-
.replaceAll('"', '"')
|
|
2972
|
-
.replaceAll("'", ''');
|
|
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
|
-
}
|