mirror of
https://github.com/kunkunsh/kunkun.git
synced 2025-07-08 00:51:31 +00:00

* fix: update email field in KunkunExtManifest to be optional and nullable * refactor: update Supabase type generation command and enhance database types structure - Changed the Supabase type generation command to include a specific project reference and output path. - Refactored the database types in `database.types.ts` for improved readability and added new fields, including `extension_state` and `package_json` in the `ext_publish` table. - Ensured consistent formatting across type definitions for better maintainability. * feat: add optional README path to ExtPackageJson and enhance tests for README retrieval * feat: add optional readmeContent to ExtensionPublishValidationData and retrieve README in validateJsrPackageAsKunkunExtension * feat: add optional readme field to database types for improved package metadata * feat: enhance StoreExtDetail to display package metadata including author and contributors - Added packageJson prop to StoreExtDetail for improved extension metadata display. - Implemented rendering of author and contributors from packageJson. - Integrated README content display in StoreExtDetail if available. - Updated +page.svelte to parse and provide packageJson data using valibot for validation. * feat: enhance TauriLink component to support conditional rendering based on Tauri environment - Added detection for Tauri environment using the browser variable. - Updated the TauriLink component to render a button when in Tauri, and an anchor tag for external links otherwise. - Improved user experience by ensuring appropriate link behavior based on the application context. * feat: add unpacked size to npm registry * feat: replace size in ext_publish table to tarball_size, add unpacked_size (only applicable to npm) * feat: add pretty-bytes dependency and update debug package version - Added `pretty-bytes` package with version 6.1.1 to `package.json`. - Updated `debug` package to use `supports-color@9.4.0` in `pnpm-lock.yaml` for improved compatibility. * feat: add tarball_size field to database types for improved package metadata * feat: add readme fetching for npm registry, readme from github * fix: remove console.log from NPM API test to clean up output * style: update extension store details * style: update README section in StoreExtDetail component for improved styling * fix: update command input placeholder text in English, Russian, and Chinese translations for clarity * chore: bump version to 0.1.18 in package.json * fix: lint
261 lines
8.7 KiB
TypeScript
261 lines
8.7 KiB
TypeScript
import { ExtPackageJson, License } from "@kksh/api/models"
|
|
import * as v from "valibot"
|
|
import {
|
|
authenticatedUserIsMemberOfGitHubOrg,
|
|
parseGitHubRepoFromUri,
|
|
userIsPublicMemberOfGitHubOrg
|
|
} from "../github"
|
|
import type { ExtensionPublishValidationData } from "../models"
|
|
import { getInfoFromRekorLog } from "../sigstore"
|
|
import { getRawFileFromGitHub, getTarballSize } from "../utils"
|
|
import {
|
|
NpmPkgMetadata,
|
|
NpmPkgVersionMetadata,
|
|
NpmSearchResultObject,
|
|
NpmSearchResults,
|
|
Provenance
|
|
} from "./models"
|
|
|
|
export * from "./models"
|
|
|
|
/**
|
|
* Get the full metadata of an npm package
|
|
* @param pkgName
|
|
* @returns
|
|
*/
|
|
export function getFullNpmPackageInfo(pkgName: string): Promise<NpmPkgMetadata | null> {
|
|
return fetch(`https://registry.npmjs.org/${pkgName}`).then((res) => (res.ok ? res.json() : null))
|
|
}
|
|
|
|
/**
|
|
* Fetch the package.json data of an npm package
|
|
* @param pkgName
|
|
* @param version
|
|
* @returns
|
|
*/
|
|
export function getNpmPackageInfoByVersion(
|
|
pkgName: string,
|
|
version: string
|
|
): Promise<NpmPkgVersionMetadata | null> {
|
|
return fetch(`https://registry.npmjs.org/${pkgName}/${version}`).then((res) =>
|
|
res.ok ? res.json() : null
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get the provenance of an npm package
|
|
* If a package has no provenance, return null
|
|
* @param pkgName
|
|
* @param version
|
|
* @returns
|
|
*/
|
|
export function getNpmPkgProvenance(pkgName: string, version: string): Promise<Provenance | null> {
|
|
return fetch(`https://www.npmjs.com/package/${pkgName}/v/${version}/provenance`)
|
|
.then((res) => res.json())
|
|
.catch((err) => null)
|
|
}
|
|
|
|
/**
|
|
* List all packages under a scope
|
|
* @example
|
|
* To get package names under a scope, you can do:
|
|
* ```ts
|
|
* (await listPackagesOfMaintainer("huakunshen")).map((pkg) => pkg.package.name)
|
|
* ```
|
|
* @param username npm organization or username
|
|
* @returns
|
|
*/
|
|
export function listPackagesOfMaintainer(username: string): Promise<NpmSearchResultObject[]> {
|
|
return fetch(`https://registry.npmjs.org/-/v1/search?text=maintainer:${username}&size=250`, {
|
|
headers: {
|
|
"sec-fetch-dest": "document"
|
|
}
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => v.parse(NpmSearchResults, res).objects)
|
|
}
|
|
|
|
export function listPackagesOfScope(scope: string): Promise<NpmSearchResultObject[]> {
|
|
return fetch(`https://registry.npmjs.org/-/v1/search?text=${scope}&size=250`, {
|
|
headers: {
|
|
"sec-fetch-dest": "document"
|
|
}
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => v.parse(NpmSearchResults, res).objects)
|
|
}
|
|
|
|
export function getNpmPackageTarballUrl(
|
|
pkgName: string,
|
|
version: string
|
|
): Promise<string | undefined> {
|
|
return getNpmPackageInfoByVersion(pkgName, version).then((res) => res?.dist?.tarball)
|
|
}
|
|
|
|
export function npmPackageExists(pkgName: string, version: string): Promise<boolean> {
|
|
return getNpmPackageInfoByVersion(pkgName, version).then((res) => res !== null)
|
|
}
|
|
|
|
/**
|
|
* @param url Sample URL: https://search.sigstore.dev/?logIndex=153252145
|
|
* @returns
|
|
*/
|
|
function parseLogIdFromSigstoreSearchUrl(url: string): string {
|
|
const urlObj = new URL(url)
|
|
const logIndex = urlObj.searchParams.get("logIndex")
|
|
if (!logIndex) {
|
|
throw new Error("Could not parse log index from sigstore search url")
|
|
}
|
|
return logIndex
|
|
}
|
|
|
|
export async function validateNpmPackageAsKunkunExtension(payload: {
|
|
pkgName: string
|
|
version: string
|
|
githubUsername: string
|
|
tarballSizeLimit?: number
|
|
githubToken?: string
|
|
provenance?: Provenance // provenance API has cors policy, when we run this validation on client side, a provenance should be passed in
|
|
}): Promise<{
|
|
error?: string
|
|
data?: ExtensionPublishValidationData
|
|
}> {
|
|
/* -------------------------------------------------------------------------- */
|
|
/* check if npm package exist */
|
|
/* -------------------------------------------------------------------------- */
|
|
const pkgExists = await npmPackageExists(payload.pkgName, payload.version)
|
|
if (!pkgExists) {
|
|
return { error: "Package does not exist" }
|
|
}
|
|
if (!pkgExists) {
|
|
return { error: "NPM package does not exist" }
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* check if npm package has provenance */
|
|
/* -------------------------------------------------------------------------- */
|
|
const provenance =
|
|
payload.provenance ?? (await getNpmPkgProvenance(payload.pkgName, payload.version))
|
|
if (!provenance) {
|
|
return {
|
|
error: "Package doesn't have provenance, not signed by github action"
|
|
}
|
|
}
|
|
if (provenance.sourceCommitUnreachable) {
|
|
return { error: "Package's source commit is unreachable" }
|
|
}
|
|
if (provenance.sourceCommitNotFound) {
|
|
return { error: "Package's source commit is not found" }
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* get rekor sigstore */
|
|
/* -------------------------------------------------------------------------- */
|
|
if (!provenance?.summary.transparencyLogUri) {
|
|
return { error: "Package's rekor log is not found" }
|
|
}
|
|
const logIndex = parseLogIdFromSigstoreSearchUrl(provenance.summary.transparencyLogUri)
|
|
const rekorGit = await getInfoFromRekorLog(logIndex)
|
|
if (rekorGit.commit !== provenance.summary.sourceRepositoryDigest) {
|
|
return { error: "Package's rekor log commit is not the same as the source commit" }
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* check if npm pkg is linked to github repo */
|
|
/* -------------------------------------------------------------------------- */
|
|
const repoUri = provenance.summary.sourceRepositoryUri
|
|
const githubRepo = parseGitHubRepoFromUri(repoUri)
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Verify Repo Ownership */
|
|
/* -------------------------------------------------------------------------- */
|
|
if (githubRepo.owner !== payload.githubUsername) {
|
|
const isPublicMemeber = await userIsPublicMemberOfGitHubOrg(
|
|
githubRepo.owner,
|
|
payload.githubUsername
|
|
)
|
|
let isOrgMember = false
|
|
if (payload.githubToken) {
|
|
isOrgMember = await authenticatedUserIsMemberOfGitHubOrg(
|
|
githubRepo.owner,
|
|
payload.githubToken
|
|
)
|
|
}
|
|
if (!isPublicMemeber && !isOrgMember) {
|
|
return {
|
|
error: `You (${payload.githubUsername}) are not authorized to publish this package. Only ${githubRepo.owner} or its organization members can publish it.`
|
|
}
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* validate package.json format against latest schema */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
const packageJson = await getNpmPackageInfoByVersion(payload.pkgName, payload.version)
|
|
if (!packageJson) {
|
|
return { error: "Could not find package.json in NPM package" }
|
|
}
|
|
|
|
if (!packageJson.license) {
|
|
return { error: "Package license field is not found" }
|
|
}
|
|
|
|
const licenseParsed = v.safeParse(License, packageJson.license)
|
|
if (!licenseParsed.success) {
|
|
return { error: `Package license field ${packageJson.license} is not valid` }
|
|
}
|
|
|
|
const parseResult = v.safeParse(ExtPackageJson, packageJson)
|
|
if (!parseResult.success) {
|
|
console.error(v.flatten(parseResult.issues))
|
|
return { error: `package.json format not valid` }
|
|
}
|
|
/* -------------------------------------------------------------------------- */
|
|
/* get more package info */
|
|
/* -------------------------------------------------------------------------- */
|
|
const tarballUrl = packageJson.dist?.tarball
|
|
if (!tarballUrl) {
|
|
return { error: "Could not get tarball URL for NPM package" }
|
|
}
|
|
const shasum = packageJson.dist?.shasum
|
|
if (!shasum) {
|
|
return { error: "Could not get shasum for NPM package" }
|
|
}
|
|
|
|
const apiVersion = parseResult.output.dependencies?.["@kksh/api"]
|
|
if (!apiVersion) {
|
|
return {
|
|
error: `Extension ${parseResult.output.kunkun.identifier} doesn't not have @kksh/api as a dependency`
|
|
}
|
|
}
|
|
const tarballSize = await getTarballSize(tarballUrl, "GET") // NPM HEAD request doesn't support content-length
|
|
|
|
const readmeContent = await getRawFileFromGitHub(
|
|
githubRepo.owner,
|
|
githubRepo.repo,
|
|
provenance.summary.sourceRepositoryDigest,
|
|
parseResult.output.readme ?? "README.md"
|
|
)
|
|
return {
|
|
data: {
|
|
pkgJson: parseResult.output,
|
|
license: licenseParsed.output,
|
|
readmeContent,
|
|
tarballUrl,
|
|
shasum,
|
|
apiVersion,
|
|
tarballSize,
|
|
unpackedSize: packageJson.dist?.unpackedSize,
|
|
rekorLogIndex: logIndex,
|
|
github: {
|
|
githubActionInvocationId: rekorGit.githubActionInvocationId,
|
|
commit: provenance.summary.sourceRepositoryDigest,
|
|
repo: githubRepo.repo,
|
|
owner: githubRepo.owner,
|
|
workflowPath: rekorGit.workflowPath
|
|
}
|
|
}
|
|
}
|
|
}
|