diff --git a/convex/lib/packageRegistry.ts b/convex/lib/packageRegistry.ts index 9f474f77..92ec86de 100644 --- a/convex/lib/packageRegistry.ts +++ b/convex/lib/packageRegistry.ts @@ -271,6 +271,15 @@ function buildVerification(source: SourceInfo | undefined): PackageVerificationS scanStatus: "not-run", }; } + // `source.path` is the package directory inside the source repo (e.g. + // "examples/openclaw-plugin"). When the package lives at the repo root the + // CLI sends "." (or empty), and there's nothing useful to serialize. Only + // promote real subpaths into `verification.sourcePath` so consumers can + // build a `raw.githubusercontent.com////` base URL for + // resolving relative README asset references. + const rawPath = typeof source.path === "string" ? source.path.trim() : ""; + const sourcePath = + rawPath && rawPath !== "." ? rawPath.replace(/^\/+/, "").replace(/\/+$/, "") : undefined; return { tier: "source-linked", scope: "artifact-only", @@ -278,6 +287,7 @@ function buildVerification(source: SourceInfo | undefined): PackageVerificationS sourceRepo: source.repo || source.url, sourceCommit: source.commit, sourceTag: source.ref, + sourcePath: sourcePath || undefined, hasProvenance: false, scanStatus: "not-run", }; diff --git a/convex/packages.public.test.ts b/convex/packages.public.test.ts index 5431504d..a27c7823 100644 --- a/convex/packages.public.test.ts +++ b/convex/packages.public.test.ts @@ -2817,6 +2817,62 @@ describe("packages public queries", () => { }); }); + it("derives missing public verification source paths from legacy release provenance", async () => { + const verification = { + tier: "source-linked", + scope: "artifact-only", + sourceRepo: "OpenViking/OpenViking", + sourceCommit: "abcdef0123456789abcdef0123456789abcdef01", + scanStatus: "clean", + }; + const latestRelease = makeReleaseDoc({ + verification, + source: { + kind: "github", + repo: "OpenViking/OpenViking", + path: "openclaw-plugin", + }, + }); + const { ctx } = makePackageCtx({ + pkg: makePackageDoc({ + name: "@openviking/openclaw-plugin", + normalizedName: "@openviking/openclaw-plugin", + verification, + latestVersionSummary: { + version: "1.0.0", + verification, + }, + }), + latestRelease, + }); + + await expect( + getByNameHandler(ctx, { + name: "@openviking/openclaw-plugin", + }), + ).resolves.toMatchObject({ + package: { + verification: { sourcePath: "openclaw-plugin" }, + }, + latestRelease: { + verification: { sourcePath: "openclaw-plugin" }, + }, + }); + await expect( + getVersionByNameHandler(ctx, { + name: "@openviking/openclaw-plugin", + version: "1.0.0", + }), + ).resolves.toMatchObject({ + package: { + verification: { sourcePath: "openclaw-plugin" }, + }, + version: { + verification: { sourcePath: "openclaw-plugin" }, + }, + }); + }); + it("does not mark owner-readable blocked public packages as public download blocked", async () => { const { ctx } = makePackageCtx({ pkg: makePackageDoc({ diff --git a/convex/packages.ts b/convex/packages.ts index 70c7c9a5..c2a17a11 100644 --- a/convex/packages.ts +++ b/convex/packages.ts @@ -764,13 +764,29 @@ function resolvePublicPackageScanStatus( return pkg.scanStatus; } +function normalizePublicPackageSourcePath(sourcePath: unknown) { + if (typeof sourcePath !== "string") return undefined; + const trimmed = sourcePath.trim(); + if (!trimmed || trimmed === ".") return undefined; + return trimmed.replace(/^\/+/, "").replace(/\/+$/, "") || undefined; +} + +function getReleaseSourcePath(release?: Pick, "source"> | null) { + const source = release?.source; + if (!source || typeof source !== "object" || Array.isArray(source)) return undefined; + return normalizePublicPackageSourcePath((source as { path?: unknown }).path); +} + function resolvePublicPackageVerification( pkg: Pick, "verification" | "latestVersionSummary" | "scanStatus">, latestRelease?: Doc<"packageReleases"> | null, ) { const scanStatus = resolvePublicPackageScanStatus(pkg, latestRelease); const source = pkg.verification ?? pkg.latestVersionSummary?.verification; - return source && scanStatus ? { ...source, scanStatus } : source; + if (!source) return source; + const sourcePath = source.sourcePath ?? getReleaseSourcePath(latestRelease); + const verification = sourcePath ? { ...source, sourcePath } : source; + return scanStatus ? { ...verification, scanStatus } : verification; } function toPublicPackage( @@ -822,6 +838,19 @@ function omitLegacyClawScanNoteFields(release: Doc<"packageReleases">) { return publicRelease; } +function toPublicPackageRelease(release: Doc<"packageReleases">) { + const publicRelease = omitLegacyClawScanNoteFields(release); + const sourcePath = release.verification?.sourcePath ?? getReleaseSourcePath(release); + if (!release.verification || !sourcePath) return publicRelease; + return { + ...publicRelease, + verification: { + ...release.verification, + sourcePath, + }, + }; +} + function packageArtifactSummary( release: Pick< Doc<"packageReleases">, @@ -1907,7 +1936,7 @@ export const getByName = query({ package: publicPackage, latestRelease: latestRelease && !latestRelease.softDeletedAt - ? omitLegacyClawScanNoteFields(latestRelease) + ? toPublicPackageRelease(latestRelease) : null, owner, }; @@ -1948,7 +1977,7 @@ export const getManageContext = query({ return { package: pkg, - latestRelease: omitLegacyClawScanNoteFields(latestRelease), + latestRelease: toPublicPackageRelease(latestRelease), }; }, }); @@ -1978,7 +2007,7 @@ export const getByNameForStaff = query({ package: pkg, latestRelease: latestRelease && !latestRelease.softDeletedAt - ? omitLegacyClawScanNoteFields(latestRelease) + ? toPublicPackageRelease(latestRelease) : null, owner, highlighted: highlighted @@ -2012,7 +2041,7 @@ export const getByNameForViewerInternal = internalQuery({ package: publicPackage, latestRelease: latestRelease && !latestRelease.softDeletedAt - ? omitLegacyClawScanNoteFields(latestRelease) + ? toPublicPackageRelease(latestRelease) : null, owner, }; @@ -2091,7 +2120,7 @@ export const getVersionByName = query({ if (!publicPackage) return null; return { package: publicPackage, - version: omitLegacyClawScanNoteFields(release), + version: toPublicPackageRelease(release), }; }, }); @@ -2122,7 +2151,7 @@ export const getVersionByNameForViewerInternal = internalQuery({ if (!publicPackage) return null; return { package: publicPackage, - version: omitLegacyClawScanNoteFields(release), + version: toPublicPackageRelease(release), }; }, }); @@ -2159,7 +2188,7 @@ export const getVersionSecurityByNameForViewerInternal = internalQuery({ ...publicPackage, publicDownloadBlocked, }, - version: omitLegacyClawScanNoteFields(release), + version: toPublicPackageRelease(release), }; }, }); diff --git a/convex/schema.ts b/convex/schema.ts index e92da4bd..a4ab2aa6 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -361,6 +361,7 @@ const packageVerificationValidator = v.optional( sourceRepo: v.optional(v.string()), sourceCommit: v.optional(v.string()), sourceTag: v.optional(v.string()), + sourcePath: v.optional(v.string()), hasProvenance: v.optional(v.boolean()), trustedOpenClawPlugin: v.optional(v.boolean()), scanStatus: v.optional( diff --git a/e2e/helpers/clawhubCli.ts b/e2e/helpers/clawhubCli.ts index 69a4d253..4a083e16 100644 --- a/e2e/helpers/clawhubCli.ts +++ b/e2e/helpers/clawhubCli.ts @@ -152,12 +152,75 @@ function extractLastJsonObject(output: string) { throw new Error(`No JSON object in convex run output:\n${output}`); } -export async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit) { +export async function fetchWithTimeout( + input: RequestInfo | URL, + init?: RequestInit, + timeoutMs: number = REQUEST_TIMEOUT_MS, +) { const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(new Error("Timeout")), REQUEST_TIMEOUT_MS); + const timeout = setTimeout(() => controller.abort(new Error("Timeout")), timeoutMs); try { return await fetch(input, { ...init, signal: controller.signal }); } finally { clearTimeout(timeout); } } + +const MAX_RATE_LIMIT_WAIT_MS = 15_000; +const TRANSIENT_RETRY_DELAY_MS = 1_000; + +function parsePositiveNumber(value: string | null) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : null; +} + +function getRetryDelayMs(response: Response) { + const retryAfterSeconds = parsePositiveNumber(response.headers.get("Retry-After")); + if (retryAfterSeconds !== null) { + return Math.min(retryAfterSeconds * 1000, MAX_RATE_LIMIT_WAIT_MS); + } + const relativeResetSeconds = parsePositiveNumber(response.headers.get("RateLimit-Reset")); + if (relativeResetSeconds !== null) { + return Math.min(relativeResetSeconds * 1000, MAX_RATE_LIMIT_WAIT_MS); + } + const absoluteResetSeconds = parsePositiveNumber(response.headers.get("X-RateLimit-Reset")); + if (absoluteResetSeconds !== null) { + return Math.min(Math.max(absoluteResetSeconds * 1000 - Date.now(), 0), MAX_RATE_LIMIT_WAIT_MS); + } + return TRANSIENT_RETRY_DELAY_MS; +} + +/** + * Fetch with timeout, retrying on transient failures (network abort/timeout, + * 429, and 5xx). Used for read-only verification calls against a real registry + * where occasional cold starts or rate-limit hits would otherwise flake the + * test. Non-transient HTTP responses (e.g. 401/403/404) are returned as-is. + */ +export async function fetchWithRetry( + input: RequestInfo | URL, + init?: RequestInit, + options: { maxAttempts?: number; timeoutMs?: number } = {}, +) { + const maxAttempts = options.maxAttempts ?? 3; + let lastError: unknown; + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + const response = await fetchWithTimeout(input, init, options.timeoutMs); + if (attempt >= maxAttempts) return response; + if (response.status === 429) { + await new Promise((resolve) => setTimeout(resolve, getRetryDelayMs(response))); + continue; + } + if (response.status >= 500) { + await new Promise((resolve) => setTimeout(resolve, TRANSIENT_RETRY_DELAY_MS * attempt)); + continue; + } + return response; + } catch (error) { + lastError = error; + if (attempt >= maxAttempts) throw error; + await new Promise((resolve) => setTimeout(resolve, TRANSIENT_RETRY_DELAY_MS * attempt)); + } + } + throw lastError ?? new Error("fetchWithRetry exhausted attempts"); +} diff --git a/e2e/permissions.e2e.test.ts b/e2e/permissions.e2e.test.ts index 0216e258..99448fa0 100644 --- a/e2e/permissions.e2e.test.ts +++ b/e2e/permissions.e2e.test.ts @@ -10,6 +10,7 @@ import { readGlobalConfig } from "../packages/clawhub/src/config"; import { allowLiveMutations, buildE2ESkillMarkdown, + fetchWithRetry, fetchWithTimeout, getRegistry, getSite, @@ -114,7 +115,7 @@ describe("permission boundary e2e", () => { ] as const; for (const testCase of cases) { - const response = await fetchWithTimeout(new URL(testCase.path, registry), { + const response = await fetchWithRetry(new URL(testCase.path, registry), { method: testCase.method, headers: { Accept: "application/json", "Content-Type": "application/json" }, body: "body" in testCase ? JSON.stringify(testCase.body) : undefined, diff --git a/packages/clawhub/src/schema/packages.ts b/packages/clawhub/src/schema/packages.ts index ccb2dfab..0e2aa7be 100644 --- a/packages/clawhub/src/schema/packages.ts +++ b/packages/clawhub/src/schema/packages.ts @@ -52,6 +52,12 @@ export const PackageVerificationSummarySchema = type({ sourceRepo: "string?", sourceCommit: "string?", sourceTag: "string?", + // Path of the package directory inside the source repo (e.g. + // "examples/openclaw-plugin"). Forward slash separated, no leading or + // trailing slash. Used when resolving relative README asset URLs against + // raw.githubusercontent.com so that subdirectory packages render correctly. + // Absent or "." means the package lives at the repo root. + sourcePath: "string?", hasProvenance: "boolean?", scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', }); diff --git a/packages/schema/dist/packages.d.ts b/packages/schema/dist/packages.d.ts index 48a9bf7e..02cd0d9f 100644 --- a/packages/schema/dist/packages.d.ts +++ b/packages/schema/dist/packages.d.ts @@ -50,6 +50,7 @@ export declare const PackageVerificationSummarySchema: import("arktype/internal/ sourceRepo?: string | undefined; sourceCommit?: string | undefined; sourceTag?: string | undefined; + sourcePath?: string | undefined; hasProvenance?: boolean | undefined; trustedOpenClawPlugin?: boolean | undefined; scanStatus?: "clean" | "suspicious" | "malicious" | "pending" | "not-run" | undefined; @@ -421,6 +422,7 @@ export declare const ApiV1PackageResponseSchema: import("arktype/internal/varian sourceRepo?: string | undefined; sourceCommit?: string | undefined; sourceTag?: string | undefined; + sourcePath?: string | undefined; hasProvenance?: boolean | undefined; trustedOpenClawPlugin?: boolean | undefined; scanStatus?: "clean" | "suspicious" | "malicious" | "pending" | "not-run" | undefined; @@ -511,6 +513,7 @@ export declare const ApiV1PackageVersionResponseSchema: import("arktype/internal sourceRepo?: string | undefined; sourceCommit?: string | undefined; sourceTag?: string | undefined; + sourcePath?: string | undefined; hasProvenance?: boolean | undefined; trustedOpenClawPlugin?: boolean | undefined; scanStatus?: "clean" | "suspicious" | "malicious" | "pending" | "not-run" | undefined; diff --git a/packages/schema/dist/packages.js b/packages/schema/dist/packages.js index 8b4b8f64..aac862f4 100644 --- a/packages/schema/dist/packages.js +++ b/packages/schema/dist/packages.js @@ -58,6 +58,12 @@ export const PackageVerificationSummarySchema = type({ sourceRepo: "string?", sourceCommit: "string?", sourceTag: "string?", + // Path of the package directory inside the source repo (e.g. + // "examples/openclaw-plugin"). Forward slash separated, no leading or + // trailing slash. Used when resolving relative README asset URLs against + // raw.githubusercontent.com so that subdirectory packages render correctly. + // Absent or "." means the package lives at the repo root. + sourcePath: "string?", hasProvenance: "boolean?", trustedOpenClawPlugin: "boolean?", scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', diff --git a/packages/schema/dist/packages.js.map b/packages/schema/dist/packages.js.map index 3a46ce40..7773a001 100644 --- a/packages/schema/dist/packages.js.map +++ b/packages/schema/dist/packages.js.map @@ -1 +1 @@ -{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,IAAI,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEzE,MAAM,UAAU,2BAA2B,CAAC,MAAiC;IAC3E,MAAM,UAAU,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACnE,OAAO,UAAU,IAAI,SAAS,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,IAAY,EAAE,WAAsC;IAC/F,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAC1C,MAAM,aAAa,GAAG,2BAA2B,CAAC,WAAW,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,IAAI,CAAC,aAAa,IAAI,KAAK,KAAK,aAAa;QAAE,OAAO,IAAI,CAAC;IACrE,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,aAAa,CAAC;IACnE,OAAO;QACL,KAAK;QACL,aAAa;QACb,aAAa,EAAE,IAAI,aAAa,IAAI,WAAW,EAAE;QACjD,OAAO,EAAE,mBAAmB,KAAK,iCAAiC,aAAa,mBAAmB,KAAK,iCAAiC,aAAa,IAAI,WAAW,iBAAiB,SAAS,CAAC,OAAO,CAAC,eAAe,EAAE;KACzN,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC,uCAAuC,CAAC,CAAC;AAGjF,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC,kCAAkC,CAAC,CAAC;AAG7E,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAC/C,uEAAuE,CACxE,CAAC;AAGF,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC,0CAA0C,CAAC,CAAC;AAG/F,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;IAC7C,cAAc,EAAE,SAAS;IACzB,wBAAwB,EAAE,SAAS;IACnC,gBAAgB,EAAE,SAAS;IAC3B,iBAAiB,EAAE,SAAS;CAC7B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;IACjD,YAAY,EAAE,SAAS;IACvB,SAAS,EAAE,SAAS;IACpB,UAAU,EAAE,SAAS;IACrB,QAAQ,EAAE,WAAW;IACrB,SAAS,EAAE,WAAW;IACtB,KAAK,EAAE,WAAW;IAClB,aAAa,EAAE,WAAW;IAC1B,UAAU,EAAE,UAAU;IACtB,YAAY,EAAE,UAAU;IACxB,aAAa,EAAE,UAAU;IACzB,wBAAwB,EAAE,UAAU;IACpC,SAAS,EAAE,WAAW;IACtB,YAAY,EAAE,WAAW;IACzB,YAAY,EAAE,WAAW;IACzB,cAAc,EAAE,WAAW;IAC3B,cAAc,EAAE,SAAS;IACzB,YAAY,EAAE,SAAS;IACvB,WAAW,EAAE,WAAW;CACzB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;IACnD,IAAI,EAAE,6BAA6B;IACnC,KAAK,EAAE,8BAA8B;IACrC,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,SAAS;IACrB,YAAY,EAAE,SAAS;IACvB,SAAS,EAAE,SAAS;IACpB,aAAa,EAAE,UAAU;IACzB,qBAAqB,EAAE,UAAU;IACjC,UAAU,EAAE,uDAAuD;CACpE,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;IACrC,SAAS,EAAE,QAAQ;IACnB,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,QAAQ;IACf,QAAQ,EAAE,QAAQ;CACnB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC,yBAAyB,CAAC,CAAC;AAGzE,MAAM,CAAC,MAAM,mCAAmC,GAAG,IAAI,CAAC,oCAAoC,CAAC,CAAC;AAG9F,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC,gCAAgC,CAAC,CAAC;AAEhF,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC,8BAA8B,CAAC,CAAC;AAGnF,MAAM,CAAC,MAAM,6BAA6B,GAAG,yBAAyB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AAGnF,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,CAAC,8BAA8B,CAAC,CAAC;AAE9E,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC;AAGvE,MAAM,CAAC,MAAM,6BAA6B,GAAG,yBAAyB,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AAGnF,MAAM,CAAC,MAAM,mCAAmC,GAAG,IAAI,CACrD,0GAA0G,CAC3G,CAAC;AAGF,MAAM,CAAC,MAAM,uCAAuC,GAClD,mCAAmC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AAIlD,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;IAC/C,IAAI,EAAE,yBAAyB;IAC/B,MAAM,EAAE,SAAS;IACjB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,YAAY,EAAE,SAAS;IACvB,SAAS,EAAE,SAAS;IACpB,cAAc,EAAE,SAAS;IACzB,eAAe,EAAE,SAAS;IAC1B,YAAY,EAAE,SAAS;IACvB,MAAM,EAAE,YAAY;IACpB,YAAY,EAAE,yBAAyB,CAAC,QAAQ,EAAE;IAClD,cAAc,EAAE,SAAS;IACzB,WAAW,EAAE,SAAS;IACtB,OAAO,EAAE,SAAS;CACnB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;IAC/C,IAAI,EAAE,YAAY;IAClB,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,QAAQ;IACd,MAAM,EAAE,OAAO;IACf,YAAY,EAAE,QAAQ;IACtB,SAAS,EAAE,QAAQ;IACnB,cAAc,EAAE,QAAQ;IACxB,eAAe,EAAE,QAAQ;IACzB,YAAY,EAAE,QAAQ;CACvB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;IAC1C,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,SAAS;IACnB,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;IACjD,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE,SAAS;IACnB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,QAAQ;IAClB,UAAU,EAAE,SAAS;IACrB,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,OAAO,EAAE,SAAS;IAClB,WAAW,EAAE,QAAQ;IACrB,WAAW,EAAE,SAAS;IACtB,OAAO,EAAE,SAAS;IAClB,WAAW,EAAE,SAAS;CACvB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,SAAS;IAChB,QAAQ,EAAE,SAAS;IACnB,cAAc,EAAE,SAAS;IACzB,UAAU,EAAE,QAAQ;IACpB,MAAM,EAAE,8BAA8B,CAAC,KAAK,EAAE;IAC9C,cAAc,EAAE,SAAS;IACzB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,IAAI,EAAE,QAAQ;IACd,KAAK,EAAE,QAAQ;IACf,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,QAAQ;CACjB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;IAC3C,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,SAAS;IACrB,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,iCAAiC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAChE,QAAQ,EAAE,SAAS;IACnB,QAAQ,EAAE,SAAS;IACnB,mBAAmB,EAAE,YAAY;IACjC,WAAW,EAAE,UAAU;IACvB,KAAK,EAAE,SAAS;IAChB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;IAC7C,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,QAAQ;IAClB,IAAI,EAAE,QAAQ;IACd,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE,QAAQ;CACnB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;IAC1C,MAAM,EAAE,QAAQ;IAChB,WAAW,EAAE,UAAU;IACvB,QAAQ,EAAE,0BAA0B,CAAC,KAAK,EAAE;IAC5C,OAAO,EAAE,QAAQ;IACjB,aAAa,EAAE,QAAQ;IACvB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;IAC9C,EAAE,EAAE,SAAS;IACb,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,WAAW;CACzB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;IAChD,QAAQ,EAAE,kBAAkB;IAC5B,UAAU,EAAE,QAAQ;IACpB,YAAY,EAAE,QAAQ;IACtB,eAAe,EAAE,QAAQ;IACzB,iBAAiB,EAAE,QAAQ;IAC3B,gBAAgB,EAAE,QAAQ;IAC1B,WAAW,EAAE,SAAS;CACvB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5D,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAC5D,MAAM,sCAAsC,GAAG,IAAI,CAAC;AACpD,MAAM,qCAAqC,GAAG,IAAI,CAAC;AAcnD,MAAM,UAAU,mCAAmC,CACjD,KAAsC;IAEtC,OAAO,CACL,sCAAsC;QACtC,gCAAgC,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC;QAC9D,KAAK,CAAC,KAAK,CAAC,MAAM,CAChB,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,8BAA8B,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,CAAC,EAC9E,CAAC,CACF,CACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gCAAgC,CAAC,KAAsC;IACrF,OAAO,mCAAmC,CAAC,KAAK,CAAC,GAAG,2BAA2B,CAAC;AAClF,CAAC;AAED,MAAM,UAAU,4BAA4B;IAC1C,OAAO,oDAAoD,CAAC;AAC9D,CAAC;AAED,SAAS,gCAAgC,CAAC,SAAiB,EAAE,KAAa;IACxE,OAAO,qCAAqC,GAAG,cAAc,CAAC,SAAS,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;AACnG,CAAC;AAED,SAAS,8BAA8B,CACrC,SAAsC,EACtC,IAAgC;IAEhC,OAAO,CACL,IAAI,CAAC,IAAI;QACT,qCAAqC;QACrC,cAAc,CAAC,SAAS,CAAC;QACzB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;QACzB,cAAc,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAChC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAa;IACnC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACrD,MAAM,SAAS,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC3C,IAAI,SAAS,KAAK,SAAS;YAAE,SAAS;QACtC,IAAI,SAAS,GAAG,MAAM;YAAE,KAAK,IAAI,CAAC,CAAC;QACnC,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;YACtB,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,SAAS,IAAI,KAAK,EAAE,CAAC;YAC9B,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC;YAC/B,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;aAAM,CAAC;YACN,KAAK,IAAI,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,4BAA4B,GAAG;IACnC,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,SAAS;IACtB,WAAW,EAAE,SAAS;IACtB,MAAM,EAAE,mBAAmB;IAC3B,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,QAAQ;IACnB,oBAAoB,EAAE,SAAS;IAC/B,OAAO,EAAE,oBAAoB,CAAC,QAAQ,EAAE;IACxC,IAAI,EAAE,WAAW;IACjB,MAAM,EAAE,mBAAmB,CAAC,QAAQ,EAAE;IACtC,MAAM,EAAE,2BAA2B,CAAC,QAAQ,EAAE;CACtC,CAAC;AAEX,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;IAC/C,GAAG,EAAE,QAAQ;IACb,GAAG,4BAA4B;CAChC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,GAAG,EAAE,QAAQ;IACb,GAAG,4BAA4B;IAC/B,QAAQ,EAAE,4BAA4B,CAAC,QAAQ,EAAE;IACjD,KAAK,EAAE,oBAAoB,CAAC,KAAK,EAAE;CACpC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;IACxC,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,QAAQ;IACrB,MAAM,EAAE,mBAAmB;IAC3B,SAAS,EAAE,cAAc;IACzB,OAAO,EAAE,oBAAoB;IAC7B,UAAU,EAAE,SAAS;IACrB,OAAO,EAAE,cAAc;IACvB,WAAW,EAAE,cAAc;IAC3B,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;IACnB,aAAa,EAAE,cAAc;IAC7B,cAAc,EAAE,WAAW;IAC3B,YAAY,EAAE,UAAU;IACxB,gBAAgB,EAAE,6BAA6B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;CACtE,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;IACjD,KAAK,EAAE,qBAAqB,CAAC,KAAK,EAAE;IACpC,UAAU,EAAE,aAAa;CAC1B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;IACnD,OAAO,EAAE,IAAI,CAAC;QACZ,KAAK,EAAE,QAAQ;QACf,OAAO,EAAE,qBAAqB;KAC/B,CAAC,CAAC,KAAK,EAAE;CACX,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;IAC7C,OAAO,EAAE,IAAI,CAAC;QACZ,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;QAC3B,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,oBAAoB;QAC7B,UAAU,EAAE,SAAS;QACrB,OAAO,EAAE,cAAc;QACvB,WAAW,EAAE,cAAc;QAC3B,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,cAAc;QAC7B,IAAI,EAAE,SAAS;QACf,aAAa,EAAE,0BAA0B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC/D,YAAY,EAAE,8BAA8B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAClE,YAAY,EAAE,gCAAgC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QACpE,QAAQ,EAAE,4BAA4B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC5D,UAAU,EAAE,uDAAuD;QACnE,KAAK,EAAE,kBAAkB,CAAC,QAAQ,EAAE;KACrC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;IACb,KAAK,EAAE,IAAI,CAAC;QACV,MAAM,EAAE,aAAa;QACrB,WAAW,EAAE,cAAc;QAC3B,KAAK,EAAE,cAAc;KACtB,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;CACd,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,qCAAqC,GAAG,IAAI,CAAC;IACxD,KAAK,EAAE,IAAI,CAAC;QACV,OAAO,EAAE,QAAQ;QACjB,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,QAAQ;QACnB,QAAQ,EAAE,WAAW;KACtB,CAAC,CAAC,KAAK,EAAE;IACV,UAAU,EAAE,aAAa;CAC1B,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,OAAO,EAAE,IAAI,CAAC;QACZ,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;KAC5B,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;IACb,OAAO,EAAE,IAAI,CAAC;QACZ,OAAO,EAAE,QAAQ;QACjB,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,QAAQ;QACnB,QAAQ,EAAE,WAAW;QACrB,KAAK,EAAE,SAAS;QAChB,aAAa,EAAE,0BAA0B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC/D,YAAY,EAAE,8BAA8B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAClE,YAAY,EAAE,gCAAgC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QACpE,QAAQ,EAAE,4BAA4B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC5D,UAAU,EAAE,cAAc;QAC1B,UAAU,EAAE,uBAAuB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QACzD,oBAAoB,EAAE,iCAAiC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC7E,WAAW,EAAE,wBAAwB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC3D,UAAU,EAAE,uBAAuB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;KAC1D,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;CACd,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,kCAAkC,GAAG,IAAI,CAAC;IACrD,OAAO,EAAE,IAAI,CAAC;QACZ,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;KAC5B,CAAC;IACF,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE,IAAI,CAAC;QACb,IAAI,EAAE,yBAAyB;QAC/B,MAAM,EAAE,SAAS;QACjB,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,SAAS;QACjB,YAAY,EAAE,SAAS;QACvB,SAAS,EAAE,SAAS;QACpB,cAAc,EAAE,SAAS;QACzB,eAAe,EAAE,SAAS;QAC1B,YAAY,EAAE,SAAS;QACvB,WAAW,EAAE,QAAQ;QACrB,UAAU,EAAE,SAAS;QACrB,iBAAiB,EAAE,SAAS;QAC5B,MAAM,EAAE,YAAY;QACpB,YAAY,EAAE,yBAAyB,CAAC,QAAQ,EAAE;QAClD,cAAc,EAAE,SAAS;QACzB,WAAW,EAAE,SAAS;QACtB,OAAO,EAAE,SAAS;KACnB,CAAC;CACH,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,kCAAkC,GAAG,IAAI,CAAC;IACrD,OAAO,EAAE,IAAI,CAAC;QACZ,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;KAC5B,CAAC;IACF,OAAO,EAAE,IAAI,CAAC;QACZ,SAAS,EAAE,QAAQ;QACnB,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,yBAAyB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC7D,cAAc,EAAE,SAAS;QACzB,YAAY,EAAE,SAAS;QACvB,SAAS,EAAE,SAAS;QACpB,cAAc,EAAE,SAAS;QACzB,SAAS,EAAE,QAAQ;KACpB,CAAC;IACF,KAAK,EAAE,IAAI,CAAC;QACV,UAAU,EAAE,sDAAsD;QAClE,eAAe,EAAE,mCAAmC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC1E,mBAAmB,EAAE,SAAS;QAC9B,OAAO,EAAE,UAAU;QACnB,OAAO,EAAE,SAAS;QAClB,KAAK,EAAE,SAAS;KACjB,CAAC;CACH,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,qCAAqC,GAAG,IAAI,CAAC;IACxD,KAAK,EAAE,mCAAmC;IAC1C,MAAM,EAAE,QAAQ;CACjB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;CACnB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;IACnD,EAAE,EAAE,MAAM;IACV,QAAQ,EAAE,SAAS;IACnB,eAAe,EAAE,SAAS;IAC1B,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,aAAa;IACxB,WAAW,EAAE,QAAQ;CACtB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;IACnD,MAAM,EAAE,yBAAyB;IACjC,IAAI,EAAE,SAAS;IACf,WAAW,EAAE,8BAA8B,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;IAC7C,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,QAAQ;CAClB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;IACnD,EAAE,EAAE,MAAM;IACV,SAAS,EAAE,SAAS;IACpB,WAAW,EAAE,SAAS;IACtB,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,yBAAyB;CAClC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,MAAM,EAAE,yBAAyB;IACjC,IAAI,EAAE,SAAS;IACf,WAAW,EAAE,8BAA8B,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,oCAAoC,GAAG,IAAI,CAAC;IACvD,KAAK,EAAE,IAAI,CAAC;QACV,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,QAAQ;QACnB,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,QAAQ;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE,yBAAyB;QACjC,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,IAAI,CAAC;YACd,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,cAAc;YACtB,WAAW,EAAE,cAAc;SAC5B,CAAC;QACF,UAAU,EAAE,cAAc;QAC1B,UAAU,EAAE,cAAc;QAC1B,cAAc,EAAE,cAAc;QAC9B,WAAW,EAAE,8BAA8B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;KAClE,CAAC,CAAC,KAAK,EAAE;IACV,UAAU,EAAE,aAAa;IACzB,IAAI,EAAE,SAAS;CAChB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,uCAAuC,GAAG,IAAI,CAAC;IAC1D,EAAE,EAAE,MAAM;IACV,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,yBAAyB;IACjC,WAAW,EAAE,8BAA8B,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,oCAAoC,GAAG,IAAI,CAAC;IACvD,KAAK,EAAE,IAAI,CAAC;QACV,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,cAAc;QACzB,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,cAAc;QACvB,MAAM,EAAE,cAAc;QACtB,MAAM,EAAE,yBAAyB;QACjC,SAAS,EAAE,QAAQ;QACnB,QAAQ,EAAE,IAAI,CAAC;YACb,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,cAAc;YACtB,WAAW,EAAE,cAAc;SAC5B,CAAC;QACF,SAAS,EAAE,cAAc;QACzB,SAAS,EAAE,cAAc;QACzB,UAAU,EAAE,cAAc;QAC1B,WAAW,EAAE,8BAA8B,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;KAClE,CAAC,CAAC,KAAK,EAAE;IACV,UAAU,EAAE,aAAa;IACzB,IAAI,EAAE,SAAS;CAChB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,sCAAsC,GAAG,IAAI,CAAC;IACzD,EAAE,EAAE,MAAM;IACV,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,QAAQ;IACnB,MAAM,EAAE,yBAAyB;IACjC,WAAW,EAAE,QAAQ;IACrB,WAAW,EAAE,8BAA8B,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,0CAA0C,GAAG,IAAI,CAAC;IAC7D,OAAO,EAAE,IAAI,CAAC;QACZ,SAAS,EAAE,QAAQ;QACnB,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,oBAAoB;QAC7B,UAAU,EAAE,SAAS;QACrB,WAAW,EAAE,QAAQ;QACrB,cAAc,EAAE,cAAc;QAC9B,UAAU,EAAE,uDAAuD;KACpE,CAAC;IACF,aAAa,EAAE,IAAI,CAAC;QAClB,SAAS,EAAE,QAAQ;QACnB,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,yBAAyB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC7D,UAAU,EAAE,sDAAsD;QAClE,eAAe,EAAE,mCAAmC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC1E,gBAAgB,EAAE,cAAc;QAChC,mBAAmB,EAAE,SAAS;QAC9B,OAAO,EAAE,UAAU;QACnB,SAAS,EAAE,QAAQ;KACpB,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC;CACd,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,oCAAoC,GAAG,IAAI,CAAC;IACvD,MAAM,EAAE,cAAc;IACtB,SAAS,EAAE,SAAS;IACpB,MAAM,EAAE,UAAU;CACnB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,0CAA0C,GAAG,IAAI,CAAC;IAC7D,EAAE,EAAE,MAAM;IACV,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,QAAQ;IACjB,UAAU,EAAE,aAAa;IACzB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;CAClB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;IAC9C,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,QAAQ;IACf,MAAM,EAAE,sBAAsB;IAC9B,OAAO,EAAE,QAAQ;CAClB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,mCAAmC,GAAG,IAAI,CAAC;IACtD,OAAO,EAAE,IAAI,CAAC;QACZ,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;QAC3B,UAAU,EAAE,SAAS;QACrB,aAAa,EAAE,cAAc;KAC9B,CAAC;IACF,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,2BAA2B,CAAC,KAAK,EAAE;IAC3C,QAAQ,EAAE,UAAU;CACrB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;IAC/C,OAAO,EAAE,QAAQ;IACjB,MAAM,EAAE,SAAS;CAClB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,kCAAkC,GAAG,IAAI,CAAC;IACrD,EAAE,EAAE,MAAM;IACV,SAAS,EAAE,QAAQ;IACnB,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,QAAQ;IACrB,gBAAgB,EAAE,SAAS;IAC3B,OAAO,EAAE,oBAAoB;IAC7B,UAAU,EAAE,SAAS;CACtB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;IACjD,QAAQ,EAAE,QAAQ;IAClB,YAAY,EAAE,UAAU;IACxB,KAAK,EAAE,SAAS;IAChB,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,UAAU;CACnB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;IACjD,SAAS,EAAE,QAAQ;IACnB,IAAI,EAAE,QAAQ;IACd,SAAS,EAAE,cAAc;IACzB,WAAW,EAAE,QAAQ;IACrB,gBAAgB,EAAE,cAAc;IAChC,OAAO,EAAE,oBAAoB;IAC7B,aAAa,EAAE,cAAc;CAC9B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,IAAI,CAAC;IACnD,MAAM,EAAE,kDAAkD;IAC1D,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,EAAE,EAAE,SAAS;IACb,KAAK,EAAE,SAAS;CACjB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,oCAAoC,GAAG,IAAI,CAAC;IACvD,EAAE,EAAE,MAAM;IACV,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,8BAA8B;IACtC,MAAM,EAAE,8BAA8B,CAAC,EAAE,CAAC,MAAM,CAAC;IACjD,WAAW,EAAE,cAAc;IAC3B,UAAU,EAAE,gCAAgC,CAAC,KAAK,EAAE;CACrD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,2CAA2C,GAAG,IAAI,CAAC;IAC9D,eAAe,EAAE,QAAQ;IACzB,WAAW,EAAE,QAAQ;IACrB,KAAK,EAAE,SAAS;IAChB,UAAU,EAAE,SAAS;IACrB,UAAU,EAAE,SAAS;IACrB,YAAY,EAAE,SAAS;IACvB,KAAK,EAAE,mCAAmC,CAAC,QAAQ,EAAE;IACrD,QAAQ,EAAE,WAAW;IACrB,mBAAmB,EAAE,UAAU;IAC/B,SAAS,EAAE,UAAU;IACrB,kBAAkB,EAAE,UAAU;IAC9B,mBAAmB,EAAE,UAAU;IAC/B,KAAK,EAAE,SAAS;CACjB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,kCAAkC,GAAG,IAAI,CAAC;IACrD,WAAW,EAAE,QAAQ;IACrB,eAAe,EAAE,QAAQ;IACzB,WAAW,EAAE,QAAQ;IACrB,SAAS,EAAE,cAAc;IACzB,KAAK,EAAE,cAAc;IACrB,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,cAAc;IAC1B,YAAY,EAAE,cAAc;IAC5B,KAAK,EAAE,mCAAmC;IAC1C,QAAQ,EAAE,UAAU;IACpB,mBAAmB,EAAE,SAAS;IAC9B,SAAS,EAAE,SAAS;IACpB,kBAAkB,EAAE,SAAS;IAC7B,mBAAmB,EAAE,SAAS;IAC9B,KAAK,EAAE,cAAc;IACrB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,+CAA+C,GAAG,IAAI,CAAC;IAClE,KAAK,EAAE,kCAAkC,CAAC,KAAK,EAAE;IACjD,UAAU,EAAE,aAAa;IACzB,IAAI,EAAE,SAAS;CAChB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,2CAA2C,GAAG,IAAI,CAAC;IAC9D,EAAE,EAAE,MAAM;IACV,SAAS,EAAE,kCAAkC;CAC9C,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,kCAAkC,GAAG,IAAI,CAAC,iCAAiC,CAAC,CAAC;AAG1F,MAAM,CAAC,MAAM,yCAAyC,GAAG,IAAI,CAAC;IAC5D,KAAK,EAAE,IAAI,CAAC;QACV,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,QAAQ;QACnB,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,QAAQ;QACrB,MAAM,EAAE,mBAAmB;QAC3B,OAAO,EAAE,oBAAoB;QAC7B,UAAU,EAAE,SAAS;QACrB,OAAO,EAAE,QAAQ;QACjB,SAAS,EAAE,QAAQ;QACnB,YAAY,EAAE,yBAAyB,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC7D,UAAU,EAAE,sDAAsD;QAClE,eAAe,EAAE,mCAAmC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;QAC1E,gBAAgB,EAAE,cAAc;QAChC,UAAU,EAAE,cAAc;QAC1B,YAAY,EAAE,cAAc;QAC5B,WAAW,EAAE,QAAQ;QACrB,cAAc,EAAE,cAAc;QAC9B,OAAO,EAAE,UAAU;KACpB,CAAC,CAAC,KAAK,EAAE;IACV,UAAU,EAAE,aAAa;IACzB,IAAI,EAAE,SAAS;CAChB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,2CAA2C,GAAG,IAAI,CAAC;IAC9D,EAAE,EAAE,MAAM;IACV,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;IACnB,KAAK,EAAE,mCAAmC;IAC1C,UAAU,EAAE,qBAAqB;CAClC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,EAAE,EAAE,MAAM;IACV,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,0CAA0C,GAAG,IAAI,CAAC;IAC7D,UAAU,EAAE,QAAQ;IACpB,gBAAgB,EAAE,QAAQ;IAC1B,WAAW,EAAE,SAAS;CACvB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,0CAA0C,GAAG,IAAI,CAAC;IAC7D,gBAAgB,EAAE,6BAA6B,CAAC,EAAE,CAAC,MAAM,CAAC;CAC3D,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;IAChD,WAAW,EAAE,QAAQ;IACrB,OAAO,EAAE,QAAQ;IACjB,eAAe,EAAE,QAAQ;CAC1B,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,mCAAmC,GAAG,IAAI,CAAC;IACtD,KAAK,EAAE,QAAQ;IACf,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/src/packages.ts b/packages/schema/src/packages.ts index 8cfeccdf..3c1355b1 100644 --- a/packages/schema/src/packages.ts +++ b/packages/schema/src/packages.ts @@ -75,6 +75,12 @@ export const PackageVerificationSummarySchema = type({ sourceRepo: "string?", sourceCommit: "string?", sourceTag: "string?", + // Path of the package directory inside the source repo (e.g. + // "examples/openclaw-plugin"). Forward slash separated, no leading or + // trailing slash. Used when resolving relative README asset URLs against + // raw.githubusercontent.com so that subdirectory packages render correctly. + // Absent or "." means the package lives at the repo root. + sourcePath: "string?", hasProvenance: "boolean?", trustedOpenClawPlugin: "boolean?", scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', diff --git a/scripts/ui-proof-publish.mjs b/scripts/ui-proof-publish.mjs index 5de0ac4a..c105982d 100644 --- a/scripts/ui-proof-publish.mjs +++ b/scripts/ui-proof-publish.mjs @@ -22,7 +22,7 @@ export function parseProofPublishArgs(argv = []) { const opts = { artifactBranch: DEFAULT_ARTIFACT_BRANCH, marker: DEFAULT_MARKER, - repo: process.env.GITHUB_REPOSITORY || DEFAULT_REPO, + repo: DEFAULT_REPO, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; diff --git a/src/__tests__/plugins-publish-route.test.tsx b/src/__tests__/plugins-publish-route.test.tsx index f9e6d626..49472b4e 100644 --- a/src/__tests__/plugins-publish-route.test.tsx +++ b/src/__tests__/plugins-publish-route.test.tsx @@ -643,4 +643,392 @@ describe("plugins publish route", () => { screen.getByRole("button", { name: "Publish plugin" }).getAttribute("disabled"), ).not.toBeNull(); }); + + it("warns when README references relative image paths but no source repo/commit is set", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File( + ['# Demo Plugin\n\n![diagram](./images/foo.png)\n\nx'], + "README.md", + { type: "text/markdown" }, + ), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/2 package-relative image paths/i)).toBeTruthy(); + }); + expect(screen.getByText(/can't resolve them to your source host/i)).toBeTruthy(); + expect(screen.getByText(/\.\/images\/foo\.png/)).toBeTruthy(); + expect(screen.getByText(/\.\/images\/bar\.png/)).toBeTruthy(); + }); + + it("warns when README picture source srcset references relative image paths", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File( + [ + '# Demo Plugin\n\nx', + ], + "README.md", + { type: "text/markdown" }, + ), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/2 package-relative image paths/i)).toBeTruthy(); + }); + expect(screen.getByText(/\.\/images\/dark\.png/)).toBeTruthy(); + expect(screen.getByText(/\.\/images\/dark@2x\.png/)).toBeTruthy(); + }); + + it("swaps the missing-source warning for a Package-path reminder once Source repo and a valid 40-hex Source commit are filled", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![diagram](./images/foo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/a package-relative image path/i)).toBeTruthy(); + }); + // Before source is filled, the missing-source copy is shown. + expect(screen.getByText(/Without Source repo \+ Commit SHA/i)).toBeTruthy(); + + fireEvent.change(screen.getByPlaceholderText("owner/repo"), { + target: { value: "openclaw/demo-plugin" }, + }); + // Use a real 40-hex SHA so buildReadmeAssetBaseUrl actually accepts it + // and the publish form's promise lines up with the renderer's behavior. + const validSha = "abc1234567890abcdef1234567890abcdef12345"; + fireEvent.change(screen.getByPlaceholderText("Full commit SHA"), { + target: { value: validSha }, + }); + + // After source is filled, the missing-source copy disappears but a softer + // reminder remains, prompting the publisher to verify Package path against + // the constructed raw.githubusercontent.com URL preview. + await waitFor(() => { + expect(screen.queryByText(/Without Source repo \+ Commit SHA/i)).toBeNull(); + }); + expect(screen.getByText(/make sure Package path matches/i)).toBeTruthy(); + expect( + screen.getByText( + new RegExp(`raw\\.githubusercontent\\.com/openclaw/demo-plugin/${validSha}/`, "i"), + ), + ).toBeTruthy(); + }); + + it("keeps the missing-source warning when Source commit is not a valid 40-hex SHA, because the renderer would silently drop the rewrite", async () => { + // Regression: the form previously accepted any non-empty Commit SHA and + // promised relative README images would be served from raw.githubusercontent.com, + // but buildReadmeAssetBaseUrl (used at render time) requires a 40-hex SHA + // and silently returns undefined for shorter or otherwise malformed input. + // The result: the publisher saw a green-light reminder, shipped, and the + // detail page 404'd. The form must now hold the renderer's validation line. + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![diagram](./images/foo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/Without Source repo \+ Commit SHA/i)).toBeTruthy(); + }); + + fireEvent.change(screen.getByPlaceholderText("owner/repo"), { + target: { value: "openclaw/demo-plugin" }, + }); + // 7-char short SHA: GitHub itself would resolve this, but our render-time + // base-URL builder rejects anything that isn't 40 hex chars, so the form + // must keep showing the missing-source copy rather than the "will be + // served from raw.githubusercontent.com" reminder. + fireEvent.change(screen.getByPlaceholderText("Full commit SHA"), { + target: { value: "abc1234" }, + }); + + // Give React a tick to recompute the warning useMemo. + await waitFor(() => { + expect(screen.getByText(/Without Source repo \+ Commit SHA/i)).toBeTruthy(); + }); + expect(screen.queryByText(/make sure Package path matches/i)).toBeNull(); + expect( + screen.queryByText(/raw\.githubusercontent\.com\/openclaw\/demo-plugin\/abc1234/i), + ).toBeNull(); + }); + + it("keeps the missing-source warning when Package path cannot be resolved safely", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![diagram](./images/foo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/Without Source repo \+ Commit SHA/i)).toBeTruthy(); + }); + + fireEvent.change(screen.getByPlaceholderText("owner/repo"), { + target: { value: "openclaw/demo-plugin" }, + }); + fireEvent.change(screen.getByPlaceholderText("Full commit SHA"), { + target: { value: "abc1234567890abcdef1234567890abcdef12345" }, + }); + fireEvent.change(screen.getByPlaceholderText("."), { + target: { value: "../demo-plugin" }, + }); + + await waitFor(() => { + expect(screen.getByText(/Without Source repo \+ Commit SHA/i)).toBeTruthy(); + }); + expect(screen.queryByText(/make sure Package path matches/i)).toBeNull(); + expect(screen.queryByText(/raw\.githubusercontent\.com\/openclaw\/demo-plugin/i)).toBeNull(); + }); + + it("stops nudging about Package path once source is filled and the README has no relative images", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![ok](https://example.com/foo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByDisplayValue("demo-plugin")).toBeTruthy(); + }); + fireEvent.change(screen.getByPlaceholderText("owner/repo"), { + target: { value: "openclaw/demo-plugin" }, + }); + fireEvent.change(screen.getByPlaceholderText("Full commit SHA"), { + target: { value: "abc1234567890abcdef1234567890abcdef12345" }, + }); + + expect(screen.queryByText(/Your README references/i)).toBeNull(); + expect(screen.queryByText(/make sure Package path matches/i)).toBeNull(); + }); + + it("keeps warning about root-absolute README image paths even when Source repo and Source commit are filled", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![logo](/static/logo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/a root-absolute image path/i)).toBeTruthy(); + }); + + fireEvent.change(screen.getByPlaceholderText("owner/repo"), { + target: { value: "openclaw/demo-plugin" }, + }); + fireEvent.change(screen.getByPlaceholderText("Full commit SHA"), { + target: { value: "abc1234567890abcdef1234567890abcdef12345" }, + }); + + // Filling in source metadata must not silence the unresolvable warning, + // because root-absolute paths are never rewritten by the renderer. + expect(screen.getByText(/a root-absolute image path/i)).toBeTruthy(); + expect(screen.getByText(/\/static\/logo\.png/)).toBeTruthy(); + }); + + it("does not warn when README only uses absolute image URLs", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![ok](https://example.com/foo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByDisplayValue("demo-plugin")).toBeTruthy(); + }); + expect(screen.queryByText(/Your README references/i)).toBeNull(); + }); + + it("clears the README relative-asset warning when the user clears the selected package", async () => { + renderPublishRoute(); + + const packageJson = withRelativePath( + new File( + [makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" })], + "package.json", + { + type: "application/json", + }, + ), + "demo-plugin/package.json", + ); + const manifest = withRelativePath( + new File(['{"id":"demo.plugin"}'], "openclaw.plugin.json", { type: "application/json" }), + "demo-plugin/openclaw.plugin.json", + ); + const readme = withRelativePath( + new File(["# Demo\n\n![diagram](./images/foo.png)\n"], "README.md", { + type: "text/markdown", + }), + "demo-plugin/README.md", + ); + + fireEvent.change(getFileInput(), { target: { files: [packageJson, manifest, readme] } }); + + await waitFor(() => { + expect(screen.getByText(/a package-relative image path/i)).toBeTruthy(); + }); + + fireEvent.click(screen.getByRole("button", { name: /Clear package/i })); + + // The Badge must not keep parroting the previous package's findings once + // the user has cleared the selection — otherwise the next pick window + // briefly shows stale warnings. + await waitFor(() => { + expect(screen.queryByText(/Your README references/i)).toBeNull(); + }); + }); }); diff --git a/src/components/MarkdownPreview.test.tsx b/src/components/MarkdownPreview.test.tsx index 5a9c149a..ab6ac9dc 100644 --- a/src/components/MarkdownPreview.test.tsx +++ b/src/components/MarkdownPreview.test.tsx @@ -64,6 +64,71 @@ describe("MarkdownPreview — raw HTML passthrough", () => { ); }); + it("leaves relative alone when no assetBaseUrl is provided", () => { + const { container } = render( + {`![diagram](./images/foo.png)`}, + ); + const img = container.querySelector("img"); + // Falls back to the legacy pass-through behavior — relative path stays as-is. + expect(img?.getAttribute("src")).toBe("./images/foo.png"); + }); + + it("resolves relative ![](./path) images against assetBaseUrl and proxies them", () => { + const { container } = render( + {`![diagram](./images/foo.png)`}, + ); + const img = container.querySelector("img"); + expect(img?.getAttribute("src")).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabc123%2Fsub%2Fimages%2Ffoo.png&w=1024&q=75", + ); + }); + + it("resolves relative in raw HTML against assetBaseUrl", () => { + const { container } = render( + {`d`}, + ); + const img = container.querySelector("img"); + expect(img?.getAttribute("src")).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabc123%2Fimages%2Ffoo.png&w=1024&q=75", + ); + }); + + it("resolves relative in raw HTML picture markup against assetBaseUrl", () => { + const { container } = render( + {`Logo`}, + ); + const source = container.querySelector("picture source"); + const img = container.querySelector("picture img"); + expect(source?.getAttribute("srcset")).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabc123%2Fdocs%2Fdark.png&w=1024&q=75 1x, /_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabc123%2Fdocs%2Fdark%402x.png&w=1024&q=75 2x", + ); + expect(img?.getAttribute("src")).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabc123%2Fdocs%2Flight.png&w=1024&q=75", + ); + }); + + it("does not rewrite root-absolute paths even when assetBaseUrl is set", () => { + const { container } = render( + {`![x](/foo.png)`}, + ); + const img = container.querySelector("img"); + // Root-absolute paths are intentionally left alone — they typically point + // at the ClawHub site itself, not at a package asset. + expect(img?.getAttribute("src")).toBe("/foo.png"); + }); + it("renders
as a real line break", () => { const container = renderMarkdown(`line one
line two`); expect(container.querySelector("br")).not.toBeNull(); diff --git a/src/components/MarkdownPreview.tsx b/src/components/MarkdownPreview.tsx index 977f3bcf..848f9301 100644 --- a/src/components/MarkdownPreview.tsx +++ b/src/components/MarkdownPreview.tsx @@ -14,6 +14,15 @@ interface MarkdownPreviewProps { /** Enable Shiki syntax highlighting for fenced code blocks. Default: true. */ highlight?: boolean; urlTransform?: UrlTransform; + /** + * Base URL used to resolve relative values inside the README + * (e.g. `./images/foo.png`). When set, relative sources are resolved + * against this base and then routed through the standard image proxy. + * Typical value: a `raw.githubusercontent.com////` URL + * derived from the package release's `verification.sourceRepo` + + * `verification.sourceCommit`. Must end with `/`. + */ + assetBaseUrl?: string; } const schema = { @@ -31,8 +40,10 @@ const schema = { // Order matters: rehype-sanitize runs BEFORE rehype-shiki so sanitize only // sees user-authored HTML; shiki's trusted styled output flows through after. // rehypeProxyImages rewrites after sanitize so we rewrite only already-safe -// nodes (sanitize strips event handlers, javascript: URLs). -const baseRehype: PluggableList = [rehypeRaw, [rehypeSanitize, schema], rehypeProxyImages]; +// image URLs (sanitize strips event handlers, javascript: URLs). +function buildBaseRehype(assetBaseUrl: string | undefined): PluggableList { + return [rehypeRaw, [rehypeSanitize, schema], [rehypeProxyImages, { assetBaseUrl }]]; +} const SHIKI_THEME = "github-dark"; const SHIKI_LANGS = [ @@ -77,6 +88,7 @@ export function MarkdownPreview({ className, highlight = true, urlTransform, + assetBaseUrl, }: MarkdownPreviewProps) { const [highlighter, setHighlighter] = useState(null); @@ -97,11 +109,12 @@ export function MarkdownPreview({ }, [highlight]); const rehypePlugins = useMemo(() => { + const baseRehype = buildBaseRehype(assetBaseUrl); if (highlight && highlighter) { return [...baseRehype, [rehypeShikiFromHighlighter, highlighter, { theme: SHIKI_THEME }]]; } return baseRehype; - }, [highlight, highlighter]); + }, [highlight, highlighter, assetBaseUrl]); return (
diff --git a/src/lib/detectRelativeReadmeAssets.test.ts b/src/lib/detectRelativeReadmeAssets.test.ts new file mode 100644 index 00000000..41779240 --- /dev/null +++ b/src/lib/detectRelativeReadmeAssets.test.ts @@ -0,0 +1,133 @@ +/* @vitest-environment node */ + +import { describe, expect, it } from "vitest"; +import { detectRelativeReadmeAssets } from "./detectRelativeReadmeAssets"; + +describe("detectRelativeReadmeAssets", () => { + it("returns nothing for empty input", () => { + expect(detectRelativeReadmeAssets("")).toEqual({ + samples: [], + total: 0, + unresolvableSamples: [], + unresolvableTotal: 0, + }); + }); + + it("flags a relative markdown image reference", () => { + const report = detectRelativeReadmeAssets("![diagram](./images/foo.png)"); + expect(report.samples).toEqual(["./images/foo.png"]); + expect(report.total).toBe(1); + expect(report.unresolvableSamples).toEqual([]); + expect(report.unresolvableTotal).toBe(0); + }); + + it("flags relative references in raw HTML", () => { + const report = detectRelativeReadmeAssets( + `x`, + ); + expect(report.samples).toEqual(["images/foo.png", "./bar.svg"]); + expect(report.total).toBe(2); + expect(report.unresolvableSamples).toEqual([]); + }); + + it("flags relative candidates in raw HTML", () => { + const report = detectRelativeReadmeAssets( + ``, + ); + expect(report.samples).toEqual(["./dark.png", "./dark@2x.png"]); + expect(report.total).toBe(2); + expect(report.unresolvableSamples).toEqual([]); + }); + + it("flags root-absolute candidates separately", () => { + const report = detectRelativeReadmeAssets( + ``, + ); + expect(report.samples).toEqual(["/dark.png", "./light.png"]); + expect(report.total).toBe(2); + expect(report.unresolvableSamples).toEqual(["/dark.png"]); + expect(report.unresolvableTotal).toBe(1); + }); + + it("flags root-absolute paths separately as unresolvable", () => { + const report = detectRelativeReadmeAssets("![logo](/static/logo.png)"); + expect(report.samples).toEqual(["/static/logo.png"]); + expect(report.total).toBe(1); + expect(report.unresolvableSamples).toEqual(["/static/logo.png"]); + expect(report.unresolvableTotal).toBe(1); + }); + + it("ignores absolute http(s) URLs", () => { + const report = detectRelativeReadmeAssets( + '![ok](https://example.com/foo.png)\n', + ); + expect(report).toEqual({ + samples: [], + total: 0, + unresolvableSamples: [], + unresolvableTotal: 0, + }); + }); + + it("ignores protocol-relative URLs, data:, mailto:, tel:, and fragment hrefs", () => { + const report = detectRelativeReadmeAssets( + [ + "![a](//cdn.example.com/x.png)", + "![b](data:image/png;base64,abc)", + "![c](#anchor)", + '', + ].join("\n"), + ); + expect(report).toEqual({ + samples: [], + total: 0, + unresolvableSamples: [], + unresolvableTotal: 0, + }); + }); + + it("deduplicates samples but counts each occurrence in total", () => { + const report = detectRelativeReadmeAssets( + '![a](./x.png)\n![a](./x.png)\n\n![b](./y.png)', + ); + expect(report.samples).toEqual(["./x.png", "./y.png"]); + expect(report.total).toBe(4); + }); + + it("counts unresolvable references in total but separates them in unresolvableSamples", () => { + const report = detectRelativeReadmeAssets( + '![rel](./images/foo.png)\n![bad](/static/logo.png)\n![bad](/static/logo.png)\n', + ); + expect(report.samples).toEqual(["./images/foo.png", "/static/logo.png", "/icons/x.svg"]); + expect(report.total).toBe(4); + expect(report.unresolvableSamples).toEqual(["/static/logo.png", "/icons/x.svg"]); + expect(report.unresolvableTotal).toBe(3); + }); + + it("caps samples at 5 distinct paths but keeps counting in total", () => { + const lines = Array.from({ length: 10 }, (_, idx) => `![n](./img-${idx}.png)`); + const report = detectRelativeReadmeAssets(lines.join("\n")); + expect(report.samples.length).toBe(5); + expect(report.samples).toEqual([ + "./img-0.png", + "./img-1.png", + "./img-2.png", + "./img-3.png", + "./img-4.png", + ]); + expect(report.total).toBe(10); + }); + + it("handles markdown image references with a title segment", () => { + const report = detectRelativeReadmeAssets(`![alt](./images/foo.png "title text")`); + expect(report.samples).toEqual(["./images/foo.png"]); + }); + + it("normalizes whitespace around raw HTML image src values", () => { + const report = detectRelativeReadmeAssets( + ``, + ); + expect(report.samples).toEqual(["./images/foo.png", "/static/logo.png"]); + expect(report.unresolvableSamples).toEqual(["/static/logo.png"]); + }); +}); diff --git a/src/lib/detectRelativeReadmeAssets.ts b/src/lib/detectRelativeReadmeAssets.ts new file mode 100644 index 00000000..005a75a0 --- /dev/null +++ b/src/lib/detectRelativeReadmeAssets.ts @@ -0,0 +1,166 @@ +/** + * Scans README markdown text for relative image references — both Markdown + * `![alt](./path)` syntax, raw HTML `` tags, and + * `` candidates — and returns the unique set of + * relative paths it finds (capped to keep UI warnings short). + * + * Why: ClawHub does not host package binary assets. When a publisher uploads + * a zip/tgz whose README references local images via relative paths, those + * images render fine inside the package but 404 on the plugin detail page + * unless the release also carries Source repo + Source commit (which lets us + * resolve them to a stable raw.githubusercontent.com URL). We use this scanner + * to surface a non-blocking warning on the publish form so authors can either + * fill in source metadata or rewrite their README to absolute URLs before + * shipping. + * + * Two flavors of "broken on the detail page" exist and the report distinguishes + * them, because the publish form needs to behave differently: + * + * - **Resolvable**: package-relative paths like `./images/foo.png` or + * `images/foo.png`. These can be rewritten at render time to + * `raw.githubusercontent.com////...` once the + * publisher fills in Source repo + Commit SHA, so the warning may be + * dismissed by completing those fields. + * - **Unresolvable**: root-absolute paths like `/static/logo.png`. The + * renderer (rehypeProxyImages) intentionally never rewrites these — there + * is no safe base URL that wouldn't accidentally pull random repo-root + * files — so they will 404 on the plugin detail page even if source + * metadata is provided. The only fixes are to rewrite them in the README + * to a real absolute URL, or to make them package-relative. + */ + +const MARKDOWN_IMAGE = /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g; +const HTML_IMG_SRC = /]*?\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*?>/gi; +const HTML_SOURCE_SRCSET = /]*?\bsrcset\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*?>/gi; + +const ABSOLUTE_URL = /^[a-z][a-z0-9+\-.]*:/i; +const PROTOCOL_RELATIVE = /^\/\//; + +const MAX_REPORTED = 5; + +function isAsciiWhitespace(char: string): boolean { + return char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f"; +} + +function classifyRelativeAsset(rawSrc: string): "package-relative" | "root-absolute" | null { + const src = rawSrc.trim(); + if (!src) return null; + if (src.startsWith("#")) return null; + if (PROTOCOL_RELATIVE.test(src)) return null; + if (ABSOLUTE_URL.test(src)) return null; + // Single leading slash (we already excluded `//...` above) means the browser + // resolves against the page origin, not the package — and the markdown + // renderer deliberately never rewrites these. Mark them out so the publish + // form can warn even when source repo + commit are both filled in. + if (src.startsWith("/")) return "root-absolute"; + return "package-relative"; +} + +export interface RelativeReadmeAssetReport { + /** Up to MAX_REPORTED unique paths, in the order encountered. */ + samples: string[]; + /** Total number of relative references detected (may exceed samples.length). */ + total: number; + /** + * Subset of `samples` that are root-absolute (e.g. `/static/logo.png`). + * These cannot be salvaged by Source repo + Commit SHA because the README + * renderer never rewrites root-absolute paths. + */ + unresolvableSamples: string[]; + /** Total number of root-absolute references detected. */ + unresolvableTotal: number; +} + +export function detectRelativeReadmeAssets(readmeText: string): RelativeReadmeAssetReport { + if (!readmeText) { + return { samples: [], total: 0, unresolvableSamples: [], unresolvableTotal: 0 }; + } + + const seen = new Set(); + const samples: string[] = []; + const unresolvableSeen = new Set(); + const unresolvableSamples: string[] = []; + let total = 0; + let unresolvableTotal = 0; + + const record = (src: string | undefined) => { + if (!src) return; + const normalizedSrc = src.trim(); + const kind = classifyRelativeAsset(normalizedSrc); + if (!kind) return; + total += 1; + if (kind === "root-absolute") unresolvableTotal += 1; + if (!seen.has(normalizedSrc)) { + seen.add(normalizedSrc); + if (samples.length < MAX_REPORTED) samples.push(normalizedSrc); + } + if (kind === "root-absolute" && !unresolvableSeen.has(normalizedSrc)) { + unresolvableSeen.add(normalizedSrc); + if (unresolvableSamples.length < MAX_REPORTED) unresolvableSamples.push(normalizedSrc); + } + }; + + const recordSrcset = (srcset: string | undefined) => { + if (!srcset) return; + let index = 0; + while (index < srcset.length) { + while (index < srcset.length) { + const char = srcset[index]; + if (isAsciiWhitespace(char) || char === ",") { + index += 1; + continue; + } + break; + } + if (index >= srcset.length) break; + + const urlStart = index; + while (index < srcset.length && !isAsciiWhitespace(srcset[index])) { + index += 1; + } + let url = srcset.slice(urlStart, index); + const endedWithComma = url.endsWith(","); + if (endedWithComma) { + url = url.slice(0, -1); + } + record(url); + + if (!endedWithComma) { + while (index < srcset.length && isAsciiWhitespace(srcset[index])) { + index += 1; + } + while (index < srcset.length && srcset[index] !== ",") { + index += 1; + } + } + if (srcset[index] === ",") { + index += 1; + } + } + }; + + MARKDOWN_IMAGE.lastIndex = 0; + for ( + let match = MARKDOWN_IMAGE.exec(readmeText); + match; + match = MARKDOWN_IMAGE.exec(readmeText) + ) { + record(match[1]); + } + + HTML_IMG_SRC.lastIndex = 0; + for (let match = HTML_IMG_SRC.exec(readmeText); match; match = HTML_IMG_SRC.exec(readmeText)) { + record(match[1] ?? match[2]); + } + + HTML_SOURCE_SRCSET.lastIndex = 0; + for ( + let match = HTML_SOURCE_SRCSET.exec(readmeText); + match; + match = HTML_SOURCE_SRCSET.exec(readmeText) + ) { + recordSrcset(match[1] ?? match[2]); + } + + return { samples, total, unresolvableSamples, unresolvableTotal }; +} diff --git a/src/lib/readmeAssetBaseUrl.test.ts b/src/lib/readmeAssetBaseUrl.test.ts new file mode 100644 index 00000000..0ac7952d --- /dev/null +++ b/src/lib/readmeAssetBaseUrl.test.ts @@ -0,0 +1,90 @@ +/* @vitest-environment node */ + +import { describe, expect, it } from "vitest"; +import { buildReadmeAssetBaseUrl } from "./readmeAssetBaseUrl"; + +const SHA = "abcdef0123456789abcdef0123456789abcdef01"; + +describe("buildReadmeAssetBaseUrl", () => { + it("builds a raw.githubusercontent.com base from owner/repo + commit SHA", () => { + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA)).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + }); + + it("normalizes a full GitHub HTTPS URL down to owner/repo", () => { + expect(buildReadmeAssetBaseUrl("https://github.com/openclaw/demo", SHA)).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + }); + + it("strips a trailing .git suffix on the source repo", () => { + expect(buildReadmeAssetBaseUrl("https://github.com/openclaw/demo.git", SHA)).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + expect(buildReadmeAssetBaseUrl("openclaw/demo.git", SHA)).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + }); + + it("returns undefined for non-GitHub source URLs", () => { + expect(buildReadmeAssetBaseUrl("https://gitlab.com/openclaw/demo", SHA)).toBeUndefined(); + }); + + it("returns undefined when sourceCommit is missing or not a 40-hex SHA", () => { + expect(buildReadmeAssetBaseUrl("openclaw/demo", undefined)).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", "")).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", "main")).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", "v1.2.3")).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA.slice(0, 10))).toBeUndefined(); + }); + + it("returns undefined when sourceRepo is missing or malformed", () => { + expect(buildReadmeAssetBaseUrl(undefined, SHA)).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("", SHA)).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("not-a-repo", SHA)).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("too/many/parts/here", SHA)).toBeUndefined(); + }); + + describe("with sourcePath", () => { + it("appends a single subdirectory after the commit SHA", () => { + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "examples/openclaw-plugin")).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/examples/openclaw-plugin/`, + ); + }); + + it("strips a leading or trailing slash on sourcePath", () => { + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "/pkg/")).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/pkg/`, + ); + }); + + it("treats '.' and empty path as repo root (no path segment)", () => { + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, ".")).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "")).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, undefined)).toBe( + `https://raw.githubusercontent.com/openclaw/demo/${SHA}/`, + ); + }); + + it("returns undefined when sourcePath contains '..' or other unsafe segments", () => { + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "../etc/passwd")).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "pkg/../escape")).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "pkg/with space")).toBeUndefined(); + expect(buildReadmeAssetBaseUrl("openclaw/demo", SHA, "pkg\\windows")).toBeUndefined(); + }); + + it("ignores sourcePath when sourceRepo or sourceCommit is invalid", () => { + expect( + buildReadmeAssetBaseUrl("not-a-repo", SHA, "examples/openclaw-plugin"), + ).toBeUndefined(); + expect( + buildReadmeAssetBaseUrl("openclaw/demo", "main", "examples/openclaw-plugin"), + ).toBeUndefined(); + }); + }); +}); diff --git a/src/lib/readmeAssetBaseUrl.ts b/src/lib/readmeAssetBaseUrl.ts new file mode 100644 index 00000000..cc8bbf7d --- /dev/null +++ b/src/lib/readmeAssetBaseUrl.ts @@ -0,0 +1,99 @@ +/** + * Build the base URL used by MarkdownPreview to resolve relative + * values (e.g. `./images/foo.png`) inside a plugin README. + * + * Strategy: when a release has source metadata (sourceRepo + a 40-hex + * sourceCommit), point at the matching `raw.githubusercontent.com` tree. + * Using the commit SHA — not a branch like `main` — keeps each published + * package page stable even if the source branch later moves. + * + * When a `sourcePath` is supplied (the package directory inside the source + * repo, e.g. `examples/openclaw-plugin`), it is appended after the commit + * SHA so that relative README images resolve against the package + * subdirectory rather than the repo root. This matters for monorepo + * publishes where the README references `./images/...` next to itself. + * + * Returns `undefined` when we can't construct a safe, stable base URL; in + * that case MarkdownPreview falls back to its legacy behavior of leaving + * relative sources untouched. + * + * `sourceRepo` may be either `owner/repo` (the canonical form) or a full + * GitHub URL (older publishes stored `source.url` here when `repo` was + * empty). Both shapes are normalized; non-GitHub hosts are rejected because + * raw.githubusercontent.com is the only host this URL pattern is valid for + * and the only host beyond GitHub already in vercel.json's image + * remotePatterns allow-list relevant to this rewrite. + * + * `sourcePath` is path-segmented and per-segment validated against a + * conservative `[A-Za-z0-9._-]` whitelist. `..` segments and any + * disallowed character cause the base URL to be rejected so the publish + * form does not promise that README images will render from the wrong + * source directory. Missing, empty, or "." source paths still mean repo root. + */ + +const COMMIT_SHA = /^[0-9a-f]{40}$/i; +const GITHUB_OWNER_REPO = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}[A-Za-z0-9])?)\/([A-Za-z0-9._-]+)$/; +const PATH_SEGMENT = /^[A-Za-z0-9._-]+$/; + +function normalizeOwnerRepo(input: string | undefined | null): string | null { + if (!input) return null; + const trimmed = input.trim(); + if (!trimmed) return null; + + if (GITHUB_OWNER_REPO.test(trimmed)) { + return trimmed.replace(/\.git$/i, ""); + } + + // Tolerate a full GitHub URL — older publishes may have stored + // `source.url` in `verification.sourceRepo`. + try { + const url = new URL(trimmed); + if (url.hostname !== "github.com" && url.hostname !== "www.github.com") return null; + const parts = url.pathname + .replace(/^\/+/, "") + .replace(/\.git$/i, "") + .split("/"); + if (parts.length < 2) return null; + const ownerRepo = `${parts[0]}/${parts[1]}`; + return GITHUB_OWNER_REPO.test(ownerRepo) ? ownerRepo : null; + } catch { + return null; + } +} + +function normalizeSourcePath(input: string | undefined | null): string | null | undefined { + if (!input) return undefined; + const trimmed = input.trim(); + if (!trimmed || trimmed === ".") return undefined; + // Reject anything that doesn't look like a forward-slash relative path — + // no protocol-like contents, no backslashes, no whitespace. Leading and + // trailing slashes are tolerated because `split("/").filter(Boolean)` + // collapses them; only segment-level shapes need strict validation. + if (/[\\\s]/.test(trimmed)) return null; + if (trimmed.includes("://")) return null; + const segments = trimmed.split("/").filter(Boolean); + if (segments.length === 0) return undefined; + for (const segment of segments) { + if (segment === "." || segment === "..") return null; + if (!PATH_SEGMENT.test(segment)) return null; + } + return segments.join("/"); +} + +export function buildReadmeAssetBaseUrl( + sourceRepo: string | undefined | null, + sourceCommit: string | undefined | null, + sourcePath?: string | null, +): string | undefined { + const ownerRepo = normalizeOwnerRepo(sourceRepo); + if (!ownerRepo) return undefined; + const commit = sourceCommit?.trim(); + if (!commit || !COMMIT_SHA.test(commit)) return undefined; + const path = normalizeSourcePath(sourcePath); + if (path === null) return undefined; + // Trailing slash is required so `new URL("./images/foo.png", base)` + // resolves as a directory rather than dropping the last path segment. + return path + ? `https://raw.githubusercontent.com/${ownerRepo}/${commit}/${path}/` + : `https://raw.githubusercontent.com/${ownerRepo}/${commit}/`; +} diff --git a/src/lib/rehypeProxyImages.test.ts b/src/lib/rehypeProxyImages.test.ts new file mode 100644 index 00000000..846fd08c --- /dev/null +++ b/src/lib/rehypeProxyImages.test.ts @@ -0,0 +1,125 @@ +/* @vitest-environment node */ + +import { describe, expect, it } from "vitest"; +import { rehypeProxyImages } from "./rehypeProxyImages"; + +type ImageTree = { + type: "root"; + children: Array<{ + type: "element"; + tagName: "img" | "source"; + properties: Record; + }>; +}; + +function rewriteImgSrc(src: string, assetBaseUrl?: string) { + const tree: ImageTree = { + type: "root", + children: [ + { + type: "element", + tagName: "img", + properties: { src }, + }, + ], + }; + rehypeProxyImages({ assetBaseUrl })(tree); + return tree.children[0].properties.src; +} + +function rewriteSourceSrcset(srcset: string, assetBaseUrl?: string, property = "srcset") { + const tree: ImageTree = { + type: "root", + children: [ + { + type: "element", + tagName: "source", + properties: { [property]: srcset }, + }, + ], + }; + rehypeProxyImages({ assetBaseUrl })(tree); + return tree.children[0].properties[property]; +} + +describe("rehypeProxyImages", () => { + it("allows relative README assets to reference parent folders inside the same commit tree", () => { + expect( + rewriteImgSrc( + "../shared/logo.png", + "https://raw.githubusercontent.com/owner/repo/abcdef/sub/", + ), + ).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Fshared%2Flogo.png&w=1024&q=75", + ); + }); + + it("does not rewrite relative README assets that escape above the commit root", () => { + expect( + rewriteImgSrc( + "../../../outside.png", + "https://raw.githubusercontent.com/owner/repo/abcdef/sub/dir/", + ), + ).toBe("../../../outside.png"); + }); + + it("does not treat explicit non-http schemes as relative README assets", () => { + expect( + rewriteImgSrc("javascript:alert(1)", "https://raw.githubusercontent.com/owner/repo/abcdef/"), + ).toBe("javascript:alert(1)"); + expect( + rewriteImgSrc("ftp://example.com/image.png", "https://raw.githubusercontent.com/x/y/z/"), + ).toBe("ftp://example.com/image.png"); + }); + + it("trims incidental whitespace before resolving relative README assets", () => { + expect( + rewriteImgSrc( + " ./images/foo.png ", + "https://raw.githubusercontent.com/owner/repo/abcdef/sub/", + ), + ).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Fsub%2Fimages%2Ffoo.png&w=1024&q=75", + ); + }); + + it("resolves relative candidates against assetBaseUrl and proxies them", () => { + expect( + rewriteSourceSrcset( + "./dark.png 1x, ./dark@2x.png 2x", + "https://raw.githubusercontent.com/owner/repo/abcdef/readme/", + ), + ).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Freadme%2Fdark.png&w=1024&q=75 1x, /_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Freadme%2Fdark%402x.png&w=1024&q=75 2x", + ); + }); + + it("rewrites supported property casing", () => { + expect( + rewriteSourceSrcset( + "wide.png 800w, wide@2x.png 1600w", + "https://raw.githubusercontent.com/owner/repo/abcdef/", + "srcSet", + ), + ).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Fwide.png&w=1024&q=75 800w, /_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Fwide%402x.png&w=1024&q=75 1600w", + ); + }); + + it("preserves unsupported entries while rewriting proxyable entries", () => { + expect( + rewriteSourceSrcset( + "data:image/svg+xml,%3Csvg%3E 1x, /site.png 2x, https://img.shields.io/badge/x-y-blue.svg 3x, ./local.png 4x", + "https://raw.githubusercontent.com/owner/repo/abcdef/", + ), + ).toBe( + "data:image/svg+xml,%3Csvg%3E 1x, /site.png 2x, /_vercel/image?url=https%3A%2F%2Fimg.shields.io%2Fbadge%2Fx-y-blue.svg&w=1024&q=75 3x, /_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fowner%2Frepo%2Fabcdef%2Flocal.png&w=1024&q=75 4x", + ); + }); + + it("leaves relative candidates alone without assetBaseUrl", () => { + expect(rewriteSourceSrcset("./dark.png 1x, ./dark@2x.png 2x")).toBe( + "./dark.png 1x, ./dark@2x.png 2x", + ); + }); +}); diff --git a/src/lib/rehypeProxyImages.ts b/src/lib/rehypeProxyImages.ts index 3a61e683..70debaea 100644 --- a/src/lib/rehypeProxyImages.ts +++ b/src/lib/rehypeProxyImages.ts @@ -5,27 +5,203 @@ type HastElementLike = { properties?: Record; }; +interface RehypeProxyImagesOptions { + /** + * Base URL used to resolve relative values (e.g. `./images/foo.png`). + * When set, relative sources are resolved against this base into an absolute + * URL and then proxied through the same /_vercel/image path as external + * sources. When unset, relative paths pass through unchanged (legacy + * behavior). + * + * Typical value: a `raw.githubusercontent.com////` URL + * built from a package release's `verification.sourceRepo` + + * `verification.sourceCommit`. Must end with `/` so it resolves as a + * directory; callers are responsible for that. + */ + assetBaseUrl?: string; +} + +const DATA_OR_FRAGMENT = /^(?:data:|#|mailto:|tel:)/i; +const ABSOLUTE_HTTP = /^https?:\/\//i; +const EXPLICIT_SCHEME = /^[a-z][a-z0-9+\-.]*:/i; +const PROTOCOL_RELATIVE = /^\/\//; +const IMAGE_PROXY_WIDTH = 1024; + +type SrcsetCandidate = { + url: string; + descriptors: string; +}; + +function getRawGitHubCommitRoot(assetBaseUrl: string): URL | null { + try { + const baseUrl = new URL(assetBaseUrl); + if (baseUrl.protocol !== "https:" || baseUrl.hostname !== "raw.githubusercontent.com") { + return null; + } + const [owner, repo, commit] = baseUrl.pathname.split("/").filter(Boolean); + if (!owner || !repo || !commit) return null; + return new URL(`/${owner}/${repo}/${commit}/`, baseUrl.origin); + } catch { + return null; + } +} + +function resolveRelativeSrc(src: string, assetBaseUrl: string | undefined): string | null { + if (!assetBaseUrl) return null; + if (!src) return null; + if (ABSOLUTE_HTTP.test(src)) return null; + if (PROTOCOL_RELATIVE.test(src)) return null; + if (DATA_OR_FRAGMENT.test(src)) return null; + if (EXPLICIT_SCHEME.test(src)) return null; + // Absolute site paths (e.g. "/foo.png") are NOT package-relative — leaving + // them alone matches how npmjs.com treats them and avoids accidentally + // pulling random repo-root files. + if (src.startsWith("/")) return null; + try { + const resolved = new URL(src, assetBaseUrl); + const commitRoot = getRawGitHubCommitRoot(assetBaseUrl); + if (!commitRoot) return null; + if (resolved.origin !== commitRoot.origin) return null; + if (!resolved.pathname.startsWith(commitRoot.pathname)) return null; + return resolved.toString(); + } catch { + return null; + } +} + +function proxyImageSrc(src: string): string { + return `/_vercel/image?url=${encodeURIComponent(src)}&w=${IMAGE_PROXY_WIDTH}&q=75`; +} + +function rewriteImageSrc(src: string, assetBaseUrl: string | undefined): string | null { + const normalizedSrc = src.trim(); + let absoluteSrc: string | null = null; + if (ABSOLUTE_HTTP.test(normalizedSrc)) { + absoluteSrc = normalizedSrc; + } else { + absoluteSrc = resolveRelativeSrc(normalizedSrc, assetBaseUrl); + } + if (!absoluteSrc) return null; + return proxyImageSrc(absoluteSrc); +} + +function isAsciiWhitespace(char: string): boolean { + return char === " " || char === "\n" || char === "\t" || char === "\r" || char === "\f"; +} + +function parseSrcset(srcset: string): SrcsetCandidate[] { + const candidates: SrcsetCandidate[] = []; + let index = 0; + + while (index < srcset.length) { + while (index < srcset.length) { + const char = srcset[index]; + if (isAsciiWhitespace(char) || char === ",") { + index += 1; + continue; + } + break; + } + if (index >= srcset.length) break; + + const urlStart = index; + while (index < srcset.length && !isAsciiWhitespace(srcset[index])) { + index += 1; + } + let url = srcset.slice(urlStart, index); + const endedWithComma = url.endsWith(","); + if (endedWithComma) { + url = url.slice(0, -1); + } + + while (index < srcset.length && isAsciiWhitespace(srcset[index])) { + index += 1; + } + + let descriptors = ""; + if (!endedWithComma) { + const descriptorsStart = index; + while (index < srcset.length && srcset[index] !== ",") { + index += 1; + } + descriptors = srcset.slice(descriptorsStart, index).trim(); + } + + candidates.push({ url, descriptors }); + + if (srcset[index] === ",") { + index += 1; + } + } + + return candidates; +} + +function rewriteSrcset(srcset: string, assetBaseUrl: string | undefined): string | null { + const candidates = parseSrcset(srcset); + if (candidates.length === 0) return null; + + let didRewrite = false; + const rewritten = candidates.map((candidate) => { + const rewrittenUrl = rewriteImageSrc(candidate.url, assetBaseUrl); + if (!rewrittenUrl) { + return candidate.descriptors ? `${candidate.url} ${candidate.descriptors}` : candidate.url; + } + didRewrite = true; + return candidate.descriptors ? `${rewrittenUrl} ${candidate.descriptors}` : rewrittenUrl; + }); + + return didRewrite ? rewritten.join(", ") : null; +} + /** - * Routes external http(s) sources through Vercel's image optimizer at + * Routes external http(s) image sources through Vercel's image optimizer at * /_vercel/image, which enforces the allow-list, SVG rejection, and caching * declared in vercel.json. Local paths, relative paths, and data: URIs pass * through unchanged — only external schemes are treated as untrusted. * + * If `assetBaseUrl` is provided, relative sources are first resolved against + * that base (typically a `raw.githubusercontent.com////` + * URL derived from the package release source metadata) and then routed + * through the same proxy. This fixes README images authored with relative + * paths like `./images/foo.png` or ``, which + * would otherwise 404 under the ClawHub route. + * * `w` is required by the optimizer and must match a value in the `sizes` * array in vercel.json, so we always pass 1024. The HTML * attribute still drives layout — this only controls served resolution. */ -export function rehypeProxyImages() { +export function rehypeProxyImages(options: RehypeProxyImagesOptions = {}) { + const { assetBaseUrl } = options; return (tree: Parameters[0]) => { visit(tree, "element", (node) => { const element = node as HastElementLike; - if (element.tagName !== "img") return; - const src = element.properties?.src; - if (typeof src !== "string" || !/^https?:\/\//i.test(src)) return; - element.properties = { - ...element.properties, - src: `/_vercel/image?url=${encodeURIComponent(src)}&w=1024&q=75`, - }; + if (element.tagName === "img") { + const src = element.properties?.src; + if (typeof src === "string") { + const rewrittenSrc = rewriteImageSrc(src, assetBaseUrl); + if (rewrittenSrc) { + element.properties = { + ...element.properties, + src: rewrittenSrc, + }; + } + } + } + + if (element.tagName === "source") { + const srcsetKey = typeof element.properties?.srcSet === "string" ? "srcSet" : "srcset"; + const srcset = element.properties?.[srcsetKey]; + if (typeof srcset === "string") { + const rewrittenSrcset = rewriteSrcset(srcset, assetBaseUrl); + if (rewrittenSrcset) { + element.properties = { + ...element.properties, + [srcsetKey]: rewrittenSrcset, + }; + } + } + } }); }; } diff --git a/src/routes/plugins/$name.tsx b/src/routes/plugins/$name.tsx index 8d4d4826..cd93e270 100644 --- a/src/routes/plugins/$name.tsx +++ b/src/routes/plugins/$name.tsx @@ -37,6 +37,7 @@ import { buildPluginSecurityAuditHref, parseScopedPackageName, } from "../../lib/pluginRoutes"; +import { buildReadmeAssetBaseUrl } from "../../lib/readmeAssetBaseUrl"; import { useAuthStatus } from "../../lib/useAuthStatus"; type PluginDetailRateLimitState = { @@ -416,6 +417,11 @@ export function PluginDetailPage({ const capabilities = latestRelease?.capabilities ?? pkg.capabilities; const compatibility = latestRelease?.compatibility ?? pkg.compatibility; const verification = latestRelease?.verification ?? pkg.verification; + const readmeAssetBaseUrl = buildReadmeAssetBaseUrl( + verification?.sourceRepo, + verification?.sourceCommit, + verification?.sourcePath, + ); const artifact = latestRelease?.artifact ?? pkg.artifact ?? null; const downloadPath = pkg.latestVersion && latestRelease?.version && artifact?.kind === "npm-pack" @@ -444,7 +450,7 @@ export function PluginDetailPage({ ? Object.entries(compatibility).filter(([, v]) => v !== undefined && v !== null) : []; const readmePanel = readme ? ( - {readme} + {readme} ) : (

No README available

diff --git a/src/routes/plugins/publish.tsx b/src/routes/plugins/publish.tsx index c7d0b269..c831dc0e 100644 --- a/src/routes/plugins/publish.tsx +++ b/src/routes/plugins/publish.tsx @@ -26,12 +26,17 @@ import { Input } from "../../components/ui/input"; import { Label } from "../../components/ui/label"; import { Textarea } from "../../components/ui/textarea"; import { VersionInput } from "../../components/VersionInput"; +import { + detectRelativeReadmeAssets, + type RelativeReadmeAssetReport, +} from "../../lib/detectRelativeReadmeAssets"; import { buildPackageUploadEntries, filterIgnoredPackageFiles, normalizePackageUploadFiles, } from "../../lib/packageUpload"; import { derivePluginPrefill, listPrefilledFields } from "../../lib/pluginPublishPrefill"; +import { buildReadmeAssetBaseUrl } from "../../lib/readmeAssetBaseUrl"; import { expandFilesWithReport } from "../../lib/uploadFiles"; import { useAuthStatus } from "../../lib/useAuthStatus"; import { formatPublishError, hashFile, uploadFile } from "../upload/-utils"; @@ -57,6 +62,49 @@ const apiRefs = api as unknown as { const SHOW_CLAWPACK_ONBOARDING_BANNER = false; const PLUGIN_PUBLISHING_GUIDE_URL = "https://docs.openclaw.ai/clawhub/publishing#plugins"; +function findReadmeFile(files: File[]): File | null { + // Match the same lookup the publish backend uses (readme.md / readme.mdx) + // by going through the shared upload-path normalizer so we see the exact + // path the server will see — including any shared-top-level-folder + // stripping. We pick the shallowest README so root-level READMEs win over + // ones nested in `examples/` etc. + const normalized = normalizePackageUploadFiles(files); + const candidates: Array<{ file: File; depth: number }> = []; + for (const entry of normalized) { + const lower = entry.path.toLowerCase(); + if (lower === "readme.md" || lower === "readme.mdx") { + candidates.push({ file: entry.file, depth: 1 }); + continue; + } + const segments = lower.split("/").filter(Boolean); + const last = segments[segments.length - 1]; + if (last === "readme.md" || last === "readme.mdx") { + candidates.push({ file: entry.file, depth: segments.length }); + } + } + if (candidates.length === 0) return null; + candidates.sort((a, b) => a.depth - b.depth); + return candidates[0]?.file ?? null; +} + +const EMPTY_README_ASSET_REPORT: RelativeReadmeAssetReport = { + samples: [], + total: 0, + unresolvableSamples: [], + unresolvableTotal: 0, +}; + +async function scanReadmeRelativeAssets(files: File[]): Promise { + const readme = findReadmeFile(files); + if (!readme) return EMPTY_README_ASSET_REPORT; + try { + const text = await readme.text(); + return detectRelativeReadmeAssets(text); + } catch { + return EMPTY_README_ASSET_REPORT; + } +} + export function PublishPluginRoute() { const search = useSearch({ from: "/plugins/publish" }); const { isAuthenticated, isLoading: isAuthLoading, me } = useAuthStatus(); @@ -83,6 +131,8 @@ export function PublishPluginRoute() { const [packageSourceKind, setPackageSourceKind] = useState(null); const [ignoredPaths, setIgnoredPaths] = useState([]); const [detectedPrefillFields, setDetectedPrefillFields] = useState([]); + const [readmeAssetReport, setReadmeAssetReport] = + useState(EMPTY_README_ASSET_REPORT); const [codePluginFieldIssues, setCodePluginFieldIssues] = useState([]); const [status, setStatus] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); @@ -163,6 +213,43 @@ export function PublishPluginRoute() { validationError, ]); + const readmeAssetWarning = useMemo(() => { + const { total, unresolvableTotal, samples, unresolvableSamples } = readmeAssetReport; + if (total === 0) return null; + const resolvableTotal = total - unresolvableTotal; + // Single source of truth: only treat the source metadata as "filled" when + // buildReadmeAssetBaseUrl — the same function the renderer uses — accepts + // it and produces a real raw.githubusercontent.com URL. This catches the + // silent-drop trap where the form previously accepted any non-empty Commit + // SHA (e.g. a 7-char short SHA, a tag like `v1.0.0`, a non-GitHub URL, or + // a `..`-laden Package path) and reassured the publisher their relative + // images would be served, while at render time COMMIT_SHA / owner-repo / + // path validation would silently drop the base URL and the detail page + // would 404. By gating on resolvedBaseUrl we keep the form's promise and + // the renderer's behavior in lock-step. + const resolvedBaseUrl = buildReadmeAssetBaseUrl(sourceRepo, sourceCommit, sourcePath); + const hasSource = Boolean(resolvedBaseUrl); + const showResolvableMissingSource = resolvableTotal > 0 && !hasSource; + const showSourcePathReminder = resolvableTotal > 0 && hasSource; + const showUnresolvable = unresolvableTotal > 0; + if (!showResolvableMissingSource && !showSourcePathReminder && !showUnresolvable) { + return null; + } + const resolvableSamples = samples.filter((sample) => !unresolvableSamples.includes(sample)); + return { + total, + samples, + resolvableTotal, + resolvableSamples, + unresolvableTotal, + unresolvableSamples, + resolvedBaseUrl, + showResolvableMissingSource, + showSourcePathReminder, + showUnresolvable, + }; + }, [readmeAssetReport, sourceRepo, sourceCommit, sourcePath]); + const onPickFiles = async (selected: File[], sourceKind: PackagePickSource) => { const expanded = await expandFilesWithReport(selected, { includeBinaryArchiveFiles: true, @@ -177,6 +264,7 @@ export function PublishPluginRoute() { setIgnoredPaths(nextIgnoredPaths); setError(null); setStatus(null); + setReadmeAssetReport(await scanReadmeRelativeAssets(filtered.files)); const prefill = await derivePluginPrefill(normalized); setDetectedPrefillFields(listPrefilledFields(prefill)); setCodePluginFieldIssues(prefill.missingRequiredFields ?? []); @@ -195,6 +283,12 @@ export function PublishPluginRoute() { setIgnoredPaths([]); setDetectedPrefillFields([]); setCodePluginFieldIssues([]); + // Without this reset the README warning Badge keeps showing the previous + // package's relative-asset findings until the next pick's async scan + // finishes — which is misleading both while no package is selected and + // during the brief window between setFiles() and setReadmeAssetReport() + // inside onPickFiles(). + setReadmeAssetReport(EMPTY_README_ASSET_REPORT); setError(null); setStatus(null); }; @@ -425,6 +519,59 @@ export function PublishPluginRoute() { disabled={metadataDisabled} onChange={(event) => setSourceCommit(event.target.value)} /> + {readmeAssetWarning ? ( + + + {readmeAssetWarning.showResolvableMissingSource ? ( + <> + Your README references{" "} + {readmeAssetWarning.resolvableTotal === 1 + ? "a package-relative image path" + : `${readmeAssetWarning.resolvableTotal} package-relative image paths`}{" "} + ({readmeAssetWarning.resolvableSamples.slice(0, 3).join(", ")} + {readmeAssetWarning.resolvableSamples.length > 3 ? ", \u2026" : ""}). + Without Source repo + Commit SHA the plugin detail page can't resolve + them to your source host, so they will 404. Fill in GitHub repository + + Commit SHA (and Package path if the package isn't at the repo root) to + serve them from raw.githubusercontent.com, or rewrite them to absolute + URLs in the README. + + ) : null} + {readmeAssetWarning.showSourcePathReminder && + readmeAssetWarning.resolvedBaseUrl ? ( + <> + Your README references{" "} + {readmeAssetWarning.resolvableTotal === 1 + ? "a package-relative image path" + : `${readmeAssetWarning.resolvableTotal} package-relative image paths`}{" "} + ({readmeAssetWarning.resolvableSamples.slice(0, 3).join(", ")} + {readmeAssetWarning.resolvableSamples.length > 3 ? ", \u2026" : ""}). + They will be served from {readmeAssetWarning.resolvedBaseUrl} — make + sure Package path matches where this package lives in the repo, or the + images will 404. + + ) : null} + {(readmeAssetWarning.showResolvableMissingSource || + readmeAssetWarning.showSourcePathReminder) && + readmeAssetWarning.showUnresolvable + ? " " + : null} + {readmeAssetWarning.showUnresolvable ? ( + <> + Your README also references{" "} + {readmeAssetWarning.unresolvableTotal === 1 + ? "a root-absolute image path" + : `${readmeAssetWarning.unresolvableTotal} root-absolute image paths`}{" "} + ({readmeAssetWarning.unresolvableSamples.slice(0, 3).join(", ")} + {readmeAssetWarning.unresolvableSamples.length > 3 ? ", \u2026" : ""}). + These start with "/" and are resolved against the page origin, not the + package, so Source repo + Commit SHA cannot rewrite them — please + replace them with absolute URLs or package-relative paths in the README. + + ) : null} + + + ) : null}