Skip to content

fix: generate asset permalinks on the canonical Commons host - #1003

Merged
jakeaturner merged 2 commits into
stagingfrom
permalink
Oct 22, 2026
Merged

jakeaturner merged 2 commits into
stagingfrom
permalink

Conversation

@AkhilTheBoss

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Severity: High — books.ts:2327: Removing denyUnlessProjectMember lets any authenticated account update another project’s page tags through /page-tags or /pages-details. The route middleware checks authentication only. Restore the membership check before writing.

Severity: High — books.ts:2109: The unauthenticated TOC endpoint now returns the unfiltered page tree, exposing titles, IDs, and URLs of Private and Semi-Private pages. Restore restriction filtering for public requests.

Severity: Medium — projectfiles.ts:689: target.endsWith(req.originalUrl) is true for ordinary /permalink/:projectID/:fileID requests regardless of their host. Consequently, noncanonical instances skip the intended redirect, and direct downloads still resolve locally. Compare the receiving origin with the canonical origin instead.

@jakeaturner
jakeaturner changed the base branch from master to staging October 15, 2026 22:38
Comment thread server/api/projectfiles.ts Outdated
{ host: receivingHost(req) },
"redirecting permalink to canonical host"
);
return res.redirect(301, target);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we originally spec'd 301 here, but let's switch to 302. This will be safer and help prevent Cloudflare and browsers caching a bad redirect indefinitely if something goes wrong. Also, let's add .setHeader("Cache-Control", "no-store") before .redirect for the same reason

Comment thread server/util/helpers.js
const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
try {
return new URL(withProtocol).origin;
} catch {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add logger.warn({ configured }, "Invalid COMMONS_CANONICAL_DOMAIN, falling back to default"); for extra visbility

if (!isCanonicalPermalinkHost(req)) {
const queryIndex = req.originalUrl.indexOf("?");
const query = queryIndex === -1 ? "" : req.originalUrl.slice(queryIndex);
const target = `${getCommonsCanonicalURL()}/permalink/${projectID}/${fileID}${query}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add

if (target.endsWith(req.originalUrl)) {
  permalinkLog.warn({ host: receivingHost(req) }, "canonical host check rejected its own origin");
} else {
 [rest of current code]
}

here. This is a cheap guard that will help us see if a mismatch on the canonical instance itself is happening

Comment thread server/api/projectfiles.ts Outdated
}

function receivingHost(req: Request) {
return (req.get("host") || req.hostname || "").toLowerCase();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove req.hostname -- it's more spoofable and technically has the possibility to differ from req.get("host")

Comment thread server/api/projectfiles.ts Outdated
}

function isCanonicalPermalinkHost(req: Request) {
const canonicalHost = new URL(getCommonsCanonicalURL()).hostname.toLowerCase();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We set a CONDUCTOR_DOMAIN env var on each instance (that is the actual domain of the instance, not the primary LibreTexts instance), so we can create a helper like this in server/util/helpers.js:

export const isCanonicalCommonsInstance = () => {
    const self = (process.env.CONDUCTOR_DOMAIN || "").trim();
    if (!self) return false;
    const withProtocol = /^https?:\/\//i.test(self) ? self : `https://${self}`;
    try {
        return new URL(withProtocol).origin === getCommonsCanonicalURL();
    } catch {
        return false;
    }
};

then we can use

if (!isCanonicalCommonsInstance()) {
  // redirect
}

in redirectPermanentLink instead of checking against the request's host

@jakeaturner
jakeaturner merged commit 12296fd into staging Oct 22, 2026
10 checks passed
@jakeaturner
jakeaturner deleted the permalink branch October 22, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants