feat(beta): public spaces (#2473)

This commit is contained in:
Philip Okugbe
2026-09-05 11:52:10 +01:00
committed by GitHub
parent f4796c982e
commit 876f3da1b2
117 changed files with 7592 additions and 715 deletions
@@ -85,8 +85,8 @@ export class ShareSeoController {
const html = fs.readFileSync(indexFilePath, 'utf8');
const transformedHtml = html
.replace(/<title>[\s\S]*?<\/title>/i, `<title>${metaTitle}</title>`)
.replace(metaTagVar, metaTags);
.replace(/<title>[\s\S]*?<\/title>/i, () => `<title>${metaTitle}</title>`)
.replace(metaTagVar, () => metaTags);
res.type('text/html').send(transformedHtml);
}
+35 -29
View File
@@ -365,35 +365,9 @@ export class ShareService {
workspaceId,
);
// Sanitize each item's content for public delivery
// generate per-attachment tokens scoped to the source page
// and strip comment marks.
const tokenized = await Promise.all(
items.map(async (item) => {
if ('status' in item) return item;
const doc = await this.prepareContentForShare(
item.content,
item.sourcePageId,
workspaceId,
);
return { ...item, content: doc?.toJSON() ?? item.content };
}),
);
// Collapse `not_found` to `no_access` for share viewers so the response
// can't be used to tell "page is shared but transclusion id doesn't
// match" from "page isn't shared at all".
const sanitized = tokenized.map((item) =>
'status' in item && item.status === 'not_found'
? {
sourcePageId: item.sourcePageId,
transclusionId: item.transclusionId,
status: 'no_access' as const,
}
: item,
);
return { items: sanitized };
return {
items: await this.sanitizeTransclusionItemsForPublic(items, workspaceId),
};
}
async isSharingAllowed(
@@ -430,6 +404,38 @@ export class ShareService {
return doc?.toJSON() ?? page.content;
}
/**
* Sanitization tail shared by every public transclusion surface: tokenize
* each content item against its source page, then hide lookup misses.
*/
async sanitizeTransclusionItemsForPublic(
items: TransclusionLookup[],
workspaceId: string,
): Promise<TransclusionLookup[]> {
const tokenized = await Promise.all(
items.map(async (item) => {
if ('status' in item) return item;
const doc = await this.prepareContentForShare(
item.content,
item.sourcePageId,
workspaceId,
);
return { ...item, content: doc?.toJSON() ?? item.content };
}),
);
// Collapse not_found to no_access so hidden sources are indistinguishable from missing ids.
return tokenized.map((item) =>
'status' in item && item.status === 'not_found'
? {
sourcePageId: item.sourcePageId,
transclusionId: item.transclusionId,
status: 'no_access' as const,
}
: item,
);
}
/**
* Prepare a ProseMirror JSON doc for delivery to a public share viewer.
* Performs the two transforms required by the share threat model: