][\s\S]*?<\/code>|][\s\S]*?<\/pre>)|(:[\w+-]+:)/gi,
+ (match, codeBlock: string | undefined, shortcode: string | undefined) => {
+ if (codeBlock) return codeBlock
+ if (shortcode) {
+ const key = shortcode.slice(1, -1)
+ return emojis[key] ?? shortcode
+ }
+ return match
+ },
+ )
}
diff --git a/shared/utils/fetch-cache-config.ts b/shared/utils/fetch-cache-config.ts
index 39a58791fb..ecd41cffd1 100644
--- a/shared/utils/fetch-cache-config.ts
+++ b/shared/utils/fetch-cache-config.ts
@@ -30,6 +30,7 @@ export const FETCH_CACHE_ALLOWED_DOMAINS = [
'gitlab.com', // GitLab API
'api.bitbucket.org', // Bitbucket API
'codeberg.org', // Codeberg (Gitea-based)
+ 'gitea.com', // Gitea API
'gitee.com', // Gitee API
// microcosm endpoints for atproto data
CONSTELLATION_HOST,
diff --git a/shared/utils/git-providers.ts b/shared/utils/git-providers.ts
index 7d2d56e86d..e4ba18dfd7 100644
--- a/shared/utils/git-providers.ts
+++ b/shared/utils/git-providers.ts
@@ -38,6 +38,18 @@ export const GITLAB_HOSTS = [
'framagit.org',
]
+/**
+ * Repo URLs come from npm package metadata, so package publishers can specify any hostname. As this
+ * is effectively user-controlled input that can point at a malicious user-controlled server, this
+ * would put us at risk of Server-Side Request Forgery (SSRF). Thus we only support allowlisted hosts.
+ */
+export const FORGEJO_HOSTS = ['next.forgejo.org', 'try.next.forgejo.org']
+
+/**
+ * No open-ended Gitea host detection for the same reason as Forgejo above.
+ */
+export const GITEA_HOSTS = ['gitea.com']
+
interface ProviderConfig {
id: ProviderId
/** Check if hostname matches this provider */
@@ -215,14 +227,7 @@ const providers: ProviderConfig[] = [
},
{
id: 'forgejo',
- matchHost: host => {
- // Match explicit Forgejo instances
- const forgejoPatterns = [/^forgejo\./i, /\.forgejo\./i]
- // Known Forgejo instances
- const knownInstances = ['next.forgejo.org', 'try.next.forgejo.org']
- if (knownInstances.some(h => host === h)) return true
- return forgejoPatterns.some(p => p.test(host))
- },
+ matchHost: host => FORGEJO_HOSTS.includes(host),
parsePath: parts => {
if (parts.length < 2) return null
const owner = decodeURIComponent(parts[0] ?? '').trim()
@@ -244,29 +249,7 @@ const providers: ProviderConfig[] = [
},
{
id: 'gitea',
- matchHost: host => {
- // Match common Gitea hosting patterns (Forgejo has its own adapter)
- const giteaPatterns = [/^git\./i, /^gitea\./i, /^code\./i, /^src\./i, /gitea\.io$/i]
- // Skip known providers (including Forgejo patterns)
- const skipHosts = [
- 'github.com',
- 'gitlab.com',
- 'codeberg.org',
- 'bitbucket.org',
- 'gitee.com',
- 'sr.ht',
- 'git.sr.ht',
- 'tangled.sh',
- 'tangled.org',
- 'next.forgejo.org',
- 'try.next.forgejo.org',
- ...GITLAB_HOSTS,
- ]
- if (skipHosts.some(h => host === h || host.endsWith(`.${h}`))) return false
- // Skip Forgejo patterns
- if (/^forgejo\./i.test(host) || /\.forgejo\./i.test(host)) return false
- return giteaPatterns.some(p => p.test(host))
- },
+ matchHost: host => GITEA_HOSTS.includes(host),
parsePath: parts => {
if (parts.length < 2) return null
const owner = decodeURIComponent(parts[0] ?? '').trim()
@@ -407,4 +390,6 @@ export const GIT_PROVIDER_API_ORIGINS = {
export const ALL_KNOWN_GIT_API_ORIGINS: readonly string[] = [
...Object.values(GIT_PROVIDER_API_ORIGINS),
...GITLAB_HOSTS.map(host => `https://${host}`),
+ ...FORGEJO_HOSTS.map(host => `https://${host}`),
+ ...GITEA_HOSTS.map(host => `https://${host}`),
]
diff --git a/shared/utils/npm.ts b/shared/utils/npm.ts
index bfb8dc7a40..a4be2e00ff 100644
--- a/shared/utils/npm.ts
+++ b/shared/utils/npm.ts
@@ -1,6 +1,7 @@
import { getLatestVersion } from 'fast-npm-meta'
import { createError } from 'h3'
import validatePackageName from 'validate-npm-package-name'
+import type { PackumentLicense } from '#shared/types/npm-registry'
const NPM_USERNAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i
const NPM_USERNAME_MAX_LENGTH = 50
@@ -62,3 +63,17 @@ export function assertValidUsername(username: string): void {
})
}
}
+
+/**
+ * Normalize a packument `license` field to a plain string.
+ * The field can be a string or an object with a `type` property.
+ *
+ * @param license Raw license value from a packument
+ * @returns License string, or `undefined` if not present or unrecognized
+ */
+export function normalizeLicense(license?: PackumentLicense): string | undefined {
+ if (!license) return undefined
+ if (typeof license === 'string') return license
+ if (typeof license.type === 'string') return license.type
+ return undefined
+}
diff --git a/shared/utils/repository-meta.ts b/shared/utils/repository-meta.ts
index c9bd4f173b..8bcf191e13 100644
--- a/shared/utils/repository-meta.ts
+++ b/shared/utils/repository-meta.ts
@@ -296,7 +296,7 @@ const giteeAdapter: ProviderAdapter = {
}
/**
- * Generic Gitea adapter for self-hosted instances.
+ * Adapter for exact allowlisted Gitea instances.
*/
const giteaAdapter: ProviderAdapter = {
links(ref) {
@@ -312,8 +312,6 @@ const giteaAdapter: ProviderAdapter = {
async fetchMeta(cachedFetch, ref, links, options = {}) {
if (!ref.host) return null
- // Note: Generic Gitea instances may not be in the allowlist,
- // so caching may not apply for self-hosted instances
let res: GiteaRepoResponse | null = null
try {
const { data } = await cachedFetch(
@@ -443,7 +441,7 @@ const radicleAdapter: ProviderAdapter = {
}
/**
- * Adapter for explicit Forgejo instances.
+ * Adapter for exact allowlisted Forgejo instances.
*/
const forgejoAdapter: ProviderAdapter = {
links(ref) {
diff --git a/test/e2e/badge.spec.ts b/test/e2e/badge.spec.ts
index 820b77d4c7..522e6bec61 100644
--- a/test/e2e/badge.spec.ts
+++ b/test/e2e/badge.spec.ts
@@ -11,6 +11,11 @@ async function fetchBadge(page: { request: { get: (url: string) => Promise
return { response, body }
}
+function getSvgWidth(body: string): number {
+ const match = body.match(/