diff --git a/scripts/docs-semantic-core.mjs b/scripts/docs-semantic-core.mjs
index 199d685..d90127b 100644
--- a/scripts/docs-semantic-core.mjs
+++ b/scripts/docs-semantic-core.mjs
@@ -1212,13 +1212,27 @@ function describeValue(value) {
}
function normalizeHeadingText(value) {
- return value
+ return stripHeadingTags(value)
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
- .replace(/<[^>]*>/g, '')
.replace(/[`*_~]/g, '')
.trim();
}
+function stripHeadingTags(value) {
+ let result = '';
+ let insideTag = false;
+ for (const character of value) {
+ if (character === '<') {
+ insideTag = true;
+ } else if (character === '>') {
+ insideTag = false;
+ } else if (!insideTag) {
+ result += character;
+ }
+ }
+ return result;
+}
+
function slugifyHeading(value) {
return value
.toLowerCase()
diff --git a/scripts/release-artifact-core.mjs b/scripts/release-artifact-core.mjs
index e100ec3..b4619ec 100644
--- a/scripts/release-artifact-core.mjs
+++ b/scripts/release-artifact-core.mjs
@@ -199,7 +199,14 @@ export function normalizeNpmViewPublicationState(value) {
}
export function inspectNpmAttestations(value, { packageName, version }) {
- const expectedSuffix = `${encodeURIComponent(packageName).replace('%40', '@')}@${version}`;
+ const packageNamePattern = /^(?:@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*|[a-z0-9][a-z0-9._~-]*)$/;
+ const validPackageName =
+ typeof packageName === 'string' &&
+ packageName.length <= 214 &&
+ packageNamePattern.test(packageName);
+ const validVersion = typeof version === 'string' && /^\d+\.\d+\.\d+$/.test(version);
+ const expectedIdentity =
+ validPackageName && validVersion ? `${packageName}@${version}` : null;
const entries = Array.isArray(value) ? value : [value];
const attestation = entries.length === 1 ? entries[0] : null;
const url = attestation?.url;
@@ -208,11 +215,10 @@ export function inspectNpmAttestations(value, { packageName, version }) {
if (entries.length !== 1) {
errors.push(`npm view must return exactly one attestation, received ${entries.length}`);
}
- if (
- typeof url !== 'string' ||
- !url.startsWith('https://registry.npmjs.org/-/npm/v1/attestations/') ||
- !decodeURIComponent(url).endsWith(expectedSuffix)
- ) {
+ if (!validPackageName || !validVersion) {
+ errors.push('npm attestation identity must use an exact package name and semantic version');
+ }
+ if (!isExactNpmAttestationUrl(url, expectedIdentity)) {
errors.push('npm attestation URL does not identify the exact package version');
}
if (predicateType !== 'https://slsa.dev/provenance/v1') {
@@ -226,6 +232,31 @@ export function inspectNpmAttestations(value, { packageName, version }) {
};
}
+function isExactNpmAttestationUrl(value, expectedIdentity) {
+ if (typeof value !== 'string' || expectedIdentity == null) {
+ return false;
+ }
+ try {
+ const parsed = new URL(value);
+ const pathPrefix = '/-/npm/v1/attestations/';
+ if (
+ parsed.protocol !== 'https:' ||
+ parsed.hostname !== 'registry.npmjs.org' ||
+ parsed.port !== '' ||
+ parsed.username !== '' ||
+ parsed.password !== '' ||
+ parsed.search !== '' ||
+ parsed.hash !== '' ||
+ !parsed.pathname.startsWith(pathPrefix)
+ ) {
+ return false;
+ }
+ return decodeURIComponent(parsed.pathname.slice(pathPrefix.length)) === expectedIdentity;
+ } catch {
+ return false;
+ }
+}
+
function publicationFailure(
version,
artifactIntegrity,
diff --git a/test/docsSemantic.test.mjs b/test/docsSemantic.test.mjs
index ec1bc90..a763685 100644
--- a/test/docsSemantic.test.mjs
+++ b/test/docsSemantic.test.mjs
@@ -494,4 +494,18 @@ describe('documentation semantic gate', () => {
{ text: 'A.B', anchor: 'ab-1' },
]);
});
+
+ it('removes nested heading tags without producing a new tag-shaped substring', () => {
+ const headings = parseHeadings(
+ '# API\n\n## <\n\n## Before and after\n\n## Unclosed