mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
feat(catalog): publish verified organization skills feed
This commit is contained in:
+104
-3
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { listOfficialEntries } from "./catalogFeed";
|
||||
import { listOfficialEntries, listOfficialSkillEntries } from "./catalogFeed";
|
||||
|
||||
vi.mock("./lib/publishers", () => ({
|
||||
getOwnerPublisher: vi.fn().mockResolvedValue({ handle: "openclaw" }),
|
||||
@@ -18,6 +18,9 @@ const listOfficialEntriesHandler = (
|
||||
unknown[]
|
||||
>
|
||||
)._handler;
|
||||
const listOfficialSkillEntriesHandler = (
|
||||
listOfficialSkillEntries as unknown as WrappedHandler<{ cursor: string | null }, unknown>
|
||||
)._handler;
|
||||
|
||||
function makePackage(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -49,7 +52,33 @@ function makeRelease(overrides: Record<string, unknown> = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeCtx(packages: unknown[], releases: Record<string, unknown>) {
|
||||
function makeSkill(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
_id: "skills:1",
|
||||
slug: "demo",
|
||||
displayName: "Demo skill",
|
||||
ownerUserId: "users:1",
|
||||
ownerPublisherId: "publishers:1",
|
||||
latestVersionId: "skillVersions:1",
|
||||
softDeletedAt: undefined,
|
||||
moderationStatus: "active",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeSkillVersion(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
_id: "skillVersions:1",
|
||||
skillId: "skills:1",
|
||||
version: "1.2.3",
|
||||
softDeletedAt: undefined,
|
||||
files: [{ path: "SKILL.md", size: 1, storageId: "storage:1", sha256: "file-hash" }],
|
||||
sha256hash: "skill-hash",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCtx(packages: unknown[], records: Record<string, unknown>) {
|
||||
return {
|
||||
db: {
|
||||
query: vi.fn(() => {
|
||||
@@ -71,7 +100,7 @@ function makeCtx(packages: unknown[], releases: Record<string, unknown>) {
|
||||
}),
|
||||
};
|
||||
}),
|
||||
get: vi.fn(async (id: string) => releases[id] ?? null),
|
||||
get: vi.fn(async (id: string) => records[id] ?? null),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -156,4 +185,76 @@ describe("catalog feed projection", () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("projects only published skills from verified organization publishers", async () => {
|
||||
const result = (await listOfficialSkillEntriesHandler(
|
||||
makeCtx([makeSkill()], {
|
||||
"publishers:1": { _id: "publishers:1", kind: "org", handle: "openclaw" },
|
||||
"skillVersions:1": makeSkillVersion(),
|
||||
}),
|
||||
{ cursor: null },
|
||||
)) as { entries: unknown[]; isDone: boolean };
|
||||
|
||||
expect(result).toMatchObject({
|
||||
entries: [
|
||||
{
|
||||
type: "skill",
|
||||
id: "@openclaw/demo",
|
||||
title: "Demo skill",
|
||||
version: "1.2.3",
|
||||
state: "available",
|
||||
publisher: { id: "openclaw", trust: "official" },
|
||||
install: {
|
||||
candidates: [
|
||||
{
|
||||
sourceRef: "public-clawhub",
|
||||
package: "@openclaw/demo",
|
||||
version: "1.2.3",
|
||||
integrity: "sha256:skill-hash",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
isDone: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("excludes a latest version blocked by the download safety gate", async () => {
|
||||
const result = (await listOfficialSkillEntriesHandler(
|
||||
makeCtx([makeSkill()], {
|
||||
"publishers:1": { _id: "publishers:1", kind: "org", handle: "openclaw" },
|
||||
"skillVersions:1": makeSkillVersion({
|
||||
llmAnalysis: { status: "complete", verdict: "malicious" },
|
||||
}),
|
||||
}),
|
||||
{ cursor: null },
|
||||
)) as { entries: unknown[]; isDone: boolean };
|
||||
|
||||
expect(result.entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("excludes personal, unverified, unpublished, and un-hashed skills", async () => {
|
||||
const skills = [
|
||||
makeSkill({ _id: "skills:user", ownerPublisherId: "publishers:user" }),
|
||||
makeSkill({ _id: "skills:unverified", ownerPublisherId: "publishers:unverified" }),
|
||||
makeSkill({ _id: "skills:unpublished", latestVersionId: undefined }),
|
||||
makeSkill({ _id: "skills:no-hash", latestVersionId: "skillVersions:no-hash" }),
|
||||
];
|
||||
vi.mocked((await import("./lib/officialPublishers")).isOfficialPublisher).mockImplementation(
|
||||
async (_ctx, publisher) => publisher?._id === "publishers:1",
|
||||
);
|
||||
|
||||
const result = (await listOfficialSkillEntriesHandler(
|
||||
makeCtx(skills, {
|
||||
"publishers:user": { _id: "publishers:user", kind: "user", handle: "alice" },
|
||||
"publishers:unverified": { _id: "publishers:unverified", kind: "org", handle: "vendor" },
|
||||
"skillVersions:1": makeSkillVersion(),
|
||||
"skillVersions:no-hash": makeSkillVersion({ sha256hash: undefined }),
|
||||
}),
|
||||
{ cursor: null },
|
||||
)) as { entries: unknown[]; isDone: boolean };
|
||||
|
||||
expect(result.entries).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
+139
-14
@@ -2,8 +2,11 @@ import {
|
||||
CATALOG_FEED_ID,
|
||||
CATALOG_FEED_SCHEMA_VERSION,
|
||||
CATALOG_FEED_SOURCE_REF,
|
||||
CATALOG_SKILLS_FEED_DESCRIPTION,
|
||||
CATALOG_SKILLS_FEED_ID,
|
||||
serializeCatalogFeed,
|
||||
type CatalogFeedEntry,
|
||||
type CatalogFeedSkillEntry,
|
||||
} from "clawhub-schema";
|
||||
import { v } from "convex/values";
|
||||
import { internal } from "./_generated/api";
|
||||
@@ -11,10 +14,16 @@ import type { Doc } from "./_generated/dataModel";
|
||||
import { internalAction, internalMutation, internalQuery } from "./_generated/server";
|
||||
import type { QueryCtx } from "./_generated/server";
|
||||
import { sha256Hex } from "./lib/clawpack";
|
||||
import { isPublicSkillDoc } from "./lib/globalStats";
|
||||
import { isOfficialPublisher } from "./lib/officialPublishers";
|
||||
import { getPackageReleaseArtifactSha256 } from "./lib/packageArtifacts";
|
||||
import { isPackageBlockedFromPublic, resolvePackageReleaseScanStatus } from "./lib/packageSecurity";
|
||||
import { getOwnerPublisher } from "./lib/publishers";
|
||||
import {
|
||||
getPublicSkillVersionDownloadBlock,
|
||||
getSkillFileModerationInfoFromSkill,
|
||||
isPublicSkillVersionAvailableForSkill,
|
||||
} from "./lib/skillFileAccess";
|
||||
|
||||
const CATALOG_FEED_DESCRIPTION = "Official OpenClaw plugins published on ClawHub.";
|
||||
const CATALOG_FEED_PAGE_SIZE = 100;
|
||||
@@ -31,8 +40,7 @@ type CatalogFeedPublicationResult = {
|
||||
entryCount: number;
|
||||
};
|
||||
|
||||
const catalogFeedEntryValidator = v.object({
|
||||
type: v.literal("plugin"),
|
||||
const catalogFeedEntryFields = {
|
||||
id: v.string(),
|
||||
title: v.string(),
|
||||
version: v.string(),
|
||||
@@ -57,7 +65,11 @@ const catalogFeedEntryValidator = v.object({
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
};
|
||||
const catalogFeedEntryValidator = v.union(
|
||||
v.object({ type: v.literal("plugin"), ...catalogFeedEntryFields }),
|
||||
v.object({ type: v.literal("skill"), ...catalogFeedEntryFields }),
|
||||
);
|
||||
|
||||
async function buildEntry(
|
||||
ctx: CatalogQueryCtx,
|
||||
@@ -139,6 +151,73 @@ async function listFamilyEntries(
|
||||
}
|
||||
}
|
||||
|
||||
async function buildSkillEntry(
|
||||
ctx: CatalogQueryCtx,
|
||||
skill: Doc<"skills">,
|
||||
): Promise<CatalogFeedSkillEntry | null> {
|
||||
if (!isPublicSkillDoc(skill) || !skill.ownerPublisherId || !skill.latestVersionId) return null;
|
||||
|
||||
const [owner, version] = await Promise.all([
|
||||
ctx.db.get(skill.ownerPublisherId),
|
||||
ctx.db.get(skill.latestVersionId),
|
||||
]);
|
||||
if (
|
||||
!owner ||
|
||||
owner.kind !== "org" ||
|
||||
!(await isOfficialPublisher(ctx, owner)) ||
|
||||
!version ||
|
||||
!isPublicSkillVersionAvailableForSkill(version, skill._id) ||
|
||||
getPublicSkillVersionDownloadBlock(getSkillFileModerationInfoFromSkill(skill), version) ||
|
||||
!version.files.length ||
|
||||
!version.sha256hash
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publisherId = owner.handle?.trim();
|
||||
const slug = skill.slug.trim();
|
||||
const title = skill.displayName.trim() || slug;
|
||||
const versionName = version.version.trim();
|
||||
if (!publisherId || !slug || !title || !versionName) return null;
|
||||
|
||||
const packageName = `@${publisherId}/${slug}`;
|
||||
return {
|
||||
type: "skill",
|
||||
id: packageName,
|
||||
title,
|
||||
version: versionName,
|
||||
state: "available",
|
||||
publisher: {
|
||||
id: publisherId,
|
||||
trust: "official",
|
||||
},
|
||||
install: {
|
||||
candidates: [
|
||||
{
|
||||
sourceRef: CATALOG_FEED_SOURCE_REF,
|
||||
package: packageName,
|
||||
version: versionName,
|
||||
integrity: `sha256:${version.sha256hash}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function listOfficialSkillFeedEntries(ctx: CatalogQueryCtx, cursor: string | null) {
|
||||
const page = await ctx.db
|
||||
.query("skills")
|
||||
.withIndex("by_active_updated", (q) => q.eq("softDeletedAt", undefined))
|
||||
.order("desc")
|
||||
.paginate({ cursor, numItems: CATALOG_FEED_PAGE_SIZE });
|
||||
const entries: CatalogFeedSkillEntry[] = [];
|
||||
for (const skill of page.page) {
|
||||
const entry = await buildSkillEntry(ctx, skill);
|
||||
if (entry) entries.push(entry);
|
||||
}
|
||||
return { entries, isDone: page.isDone, continueCursor: page.continueCursor };
|
||||
}
|
||||
|
||||
export const listOfficialEntries = internalQuery({
|
||||
args: {
|
||||
family: v.union(v.literal("code-plugin"), v.literal("bundle-plugin")),
|
||||
@@ -146,31 +225,44 @@ export const listOfficialEntries = internalQuery({
|
||||
handler: async (ctx, args) => await listFamilyEntries(ctx, args.family),
|
||||
});
|
||||
|
||||
export const listOfficialSkillEntries = internalQuery({
|
||||
args: {
|
||||
cursor: v.union(v.string(), v.null()),
|
||||
},
|
||||
handler: async (ctx, args) => await listOfficialSkillFeedEntries(ctx, args.cursor),
|
||||
});
|
||||
|
||||
export const storePublication = internalMutation({
|
||||
args: {
|
||||
feedId: v.union(v.literal(CATALOG_FEED_ID), v.literal(CATALOG_SKILLS_FEED_ID)),
|
||||
description: v.string(),
|
||||
generatedAt: v.string(),
|
||||
expiresAt: v.string(),
|
||||
entries: v.array(catalogFeedEntryValidator),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const expectedEntryType = args.feedId === CATALOG_SKILLS_FEED_ID ? "skill" : "plugin";
|
||||
if (args.entries.some((entry) => entry.type !== expectedEntryType)) {
|
||||
throw new Error(`Catalog ${expectedEntryType} feed received a mismatched entry type`);
|
||||
}
|
||||
const latest = await ctx.db
|
||||
.query("catalogFeedPublications")
|
||||
.withIndex("by_feed", (q) => q.eq("feedId", CATALOG_FEED_ID))
|
||||
.withIndex("by_feed", (q) => q.eq("feedId", args.feedId))
|
||||
.unique();
|
||||
const sequence = (latest?.sequence ?? 0) + 1;
|
||||
const payload = serializeCatalogFeed({
|
||||
schemaVersion: CATALOG_FEED_SCHEMA_VERSION,
|
||||
id: CATALOG_FEED_ID,
|
||||
id: args.feedId,
|
||||
generatedAt: args.generatedAt,
|
||||
sequence,
|
||||
expiresAt: args.expiresAt,
|
||||
description: CATALOG_FEED_DESCRIPTION,
|
||||
description: args.description,
|
||||
entries: args.entries,
|
||||
});
|
||||
const payloadSha256 = await sha256Hex(new TextEncoder().encode(payload));
|
||||
const publishedAt = Date.now();
|
||||
const publication = {
|
||||
feedId: CATALOG_FEED_ID,
|
||||
feedId: args.feedId,
|
||||
sequence,
|
||||
generatedAt: args.generatedAt,
|
||||
expiresAt: args.expiresAt,
|
||||
@@ -183,7 +275,7 @@ export const storePublication = internalMutation({
|
||||
: await ctx.db.insert("catalogFeedPublications", publication);
|
||||
return {
|
||||
publicationId,
|
||||
feedId: CATALOG_FEED_ID,
|
||||
feedId: args.feedId,
|
||||
sequence,
|
||||
payloadSha256,
|
||||
publishedAt,
|
||||
@@ -196,7 +288,7 @@ export const publish = internalAction({
|
||||
args: {
|
||||
expiresAt: v.string(),
|
||||
},
|
||||
handler: async (ctx, args): Promise<CatalogFeedPublicationResult> => {
|
||||
handler: async (ctx, args): Promise<CatalogFeedPublicationResult[]> => {
|
||||
const generatedAt = new Date().toISOString();
|
||||
const familyEntries: CatalogFeedEntry[][] = await Promise.all(
|
||||
CATALOG_FEED_FAMILIES.map(async (family) => {
|
||||
@@ -211,23 +303,56 @@ export const publish = internalAction({
|
||||
if (entries.length > MAX_CATALOG_FEED_ENTRIES) {
|
||||
throw new Error(`Catalog feed exceeds ${MAX_CATALOG_FEED_ENTRIES} entries`);
|
||||
}
|
||||
const result: CatalogFeedPublicationResult = await ctx.runMutation(
|
||||
const skillEntries: CatalogFeedSkillEntry[] = [];
|
||||
let skillCursor: string | null = null;
|
||||
let skillPageDone = false;
|
||||
while (!skillPageDone) {
|
||||
const skillPage: {
|
||||
entries: CatalogFeedSkillEntry[];
|
||||
isDone: boolean;
|
||||
continueCursor: string;
|
||||
} = await ctx.runQuery(internal.catalogFeed.listOfficialSkillEntries, {
|
||||
cursor: skillCursor,
|
||||
});
|
||||
skillEntries.push(...skillPage.entries);
|
||||
if (skillEntries.length > MAX_CATALOG_FEED_ENTRIES) {
|
||||
throw new Error(`Catalog skills feed exceeds ${MAX_CATALOG_FEED_ENTRIES} entries`);
|
||||
}
|
||||
skillPageDone = skillPage.isDone;
|
||||
skillCursor = skillPage.continueCursor;
|
||||
}
|
||||
|
||||
const pluginResult: CatalogFeedPublicationResult = await ctx.runMutation(
|
||||
internal.catalogFeed.storePublication,
|
||||
{
|
||||
feedId: CATALOG_FEED_ID,
|
||||
description: CATALOG_FEED_DESCRIPTION,
|
||||
generatedAt,
|
||||
expiresAt: args.expiresAt,
|
||||
entries: entries.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
},
|
||||
);
|
||||
return result;
|
||||
const skillsResult: CatalogFeedPublicationResult = await ctx.runMutation(
|
||||
internal.catalogFeed.storePublication,
|
||||
{
|
||||
feedId: CATALOG_SKILLS_FEED_ID,
|
||||
description: CATALOG_SKILLS_FEED_DESCRIPTION,
|
||||
generatedAt,
|
||||
expiresAt: args.expiresAt,
|
||||
entries: skillEntries.sort((left, right) => left.id.localeCompare(right.id)),
|
||||
},
|
||||
);
|
||||
return [pluginResult, skillsResult];
|
||||
},
|
||||
});
|
||||
|
||||
export const getLatestPublication = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) =>
|
||||
args: {
|
||||
feedId: v.union(v.literal(CATALOG_FEED_ID), v.literal(CATALOG_SKILLS_FEED_ID)),
|
||||
},
|
||||
handler: async (ctx, args) =>
|
||||
await ctx.db
|
||||
.query("catalogFeedPublications")
|
||||
.withIndex("by_feed", (q) => q.eq("feedId", CATALOG_FEED_ID))
|
||||
.withIndex("by_feed", (q) => q.eq("feedId", args.feedId))
|
||||
.unique(),
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
transfersGetRouterV1Http,
|
||||
banAppealContextV1Http,
|
||||
catalogFeedV1Http,
|
||||
catalogSkillsFeedV1Http,
|
||||
usersGetRouterV1Http,
|
||||
usersListV1Http,
|
||||
usersPostRouterV1Http,
|
||||
@@ -137,6 +138,12 @@ http.route({
|
||||
handler: catalogFeedV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
path: ApiRoutes.catalogSkillsFeed,
|
||||
method: "GET",
|
||||
handler: catalogSkillsFeedV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
pathPrefix: `${ApiRoutes.skills}/`,
|
||||
method: "GET",
|
||||
|
||||
@@ -35,7 +35,9 @@ describe("catalogFeedV1Handler", () => {
|
||||
expect(response.headers.get("last-modified")).toBe("Tue, 23 Jun 2026 00:00:00 GMT");
|
||||
expect(response.headers.get("cache-control")).toContain("s-maxage=300");
|
||||
expect(response.headers.get("surrogate-control")).toContain("stale-while-revalidate=86400");
|
||||
expect(ctx.runQuery).toHaveBeenCalledWith(internal.catalogFeed.getLatestPublication, {});
|
||||
expect(ctx.runQuery).toHaveBeenCalledWith(internal.catalogFeed.getLatestPublication, {
|
||||
feedId: "clawhub-official",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 304 for a matching validator", async () => {
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
import { httpAction } from "./functions";
|
||||
import { catalogFeedV1Handler } from "./httpApiV1/catalogFeedV1";
|
||||
import { catalogFeedV1Handler, catalogSkillsFeedV1Handler } from "./httpApiV1/catalogFeedV1";
|
||||
import { contentRightsV1Handler } from "./httpApiV1/contentRightsV1";
|
||||
import { verifyDocsSessionV1Handler } from "./httpApiV1/docsSessionV1";
|
||||
import {
|
||||
@@ -58,6 +58,7 @@ export const verifyDocsSessionV1Http = httpAction(verifyDocsSessionV1Handler);
|
||||
export const createPublisherV1Http = httpAction(createPublisherV1Handler);
|
||||
export const contentRightsV1Http = httpAction(contentRightsV1Handler);
|
||||
export const catalogFeedV1Http = httpAction(catalogFeedV1Handler);
|
||||
export const catalogSkillsFeedV1Http = httpAction(catalogSkillsFeedV1Handler);
|
||||
|
||||
export const searchSkillsV1Http = httpAction(searchSkillsV1Handler);
|
||||
export const resolveSkillVersionV1Http = httpAction(resolveSkillVersionV1Handler);
|
||||
@@ -100,6 +101,7 @@ export const __handlers = {
|
||||
createPublisherV1Handler,
|
||||
contentRightsV1Handler,
|
||||
catalogFeedV1Handler,
|
||||
catalogSkillsFeedV1Handler,
|
||||
searchSkillsV1Handler,
|
||||
resolveSkillVersionV1Handler,
|
||||
listSkillsV1Handler,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CATALOG_FEED_ID, CATALOG_SKILLS_FEED_ID } from "clawhub-schema";
|
||||
import { internal } from "../_generated/api";
|
||||
import type { ActionCtx } from "../_generated/server";
|
||||
import { corsHeaders, mergeHeaders } from "../lib/httpHeaders";
|
||||
@@ -23,8 +24,12 @@ function matchesLastModified(request: Request, publishedAt: number) {
|
||||
return Math.floor(publishedAt / 1000) * 1000 <= since;
|
||||
}
|
||||
|
||||
export async function catalogFeedV1Handler(ctx: ActionCtx, request: Request) {
|
||||
const publication = await ctx.runQuery(internal.catalogFeed.getLatestPublication, {});
|
||||
export async function catalogFeedV1Handler(
|
||||
ctx: ActionCtx,
|
||||
request: Request,
|
||||
feedId: typeof CATALOG_FEED_ID | typeof CATALOG_SKILLS_FEED_ID = CATALOG_FEED_ID,
|
||||
) {
|
||||
const publication = await ctx.runQuery(internal.catalogFeed.getLatestPublication, { feedId });
|
||||
if (!publication) {
|
||||
return new Response("Catalog feed is not published", {
|
||||
status: 503,
|
||||
@@ -62,3 +67,7 @@ export async function catalogFeedV1Handler(ctx: ActionCtx, request: Request) {
|
||||
}
|
||||
return new Response(publication.payload, { status: 200, headers });
|
||||
}
|
||||
|
||||
export async function catalogSkillsFeedV1Handler(ctx: ActionCtx, request: Request) {
|
||||
return await catalogFeedV1Handler(ctx, request, CATALOG_SKILLS_FEED_ID);
|
||||
}
|
||||
|
||||
+80
-2
@@ -10,6 +10,46 @@ export declare const CatalogFeedInstallCandidateSchema: import("arktype/internal
|
||||
integrity: string;
|
||||
}, {}>;
|
||||
export type CatalogFeedInstallCandidate = (typeof CatalogFeedInstallCandidateSchema)[inferred];
|
||||
export declare const CatalogFeedPluginEntrySchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
type: "plugin";
|
||||
id: string;
|
||||
title: string;
|
||||
version: string;
|
||||
state: "available" | "recommended" | "disabled" | "blocked" | "deprecated";
|
||||
publisher: {
|
||||
id: string;
|
||||
trust: "official" | "community";
|
||||
};
|
||||
install: {
|
||||
candidates: {
|
||||
sourceRef: string;
|
||||
package: string;
|
||||
version: string;
|
||||
integrity: string;
|
||||
}[];
|
||||
};
|
||||
}, {}>;
|
||||
export type CatalogFeedPluginEntry = (typeof CatalogFeedPluginEntrySchema)[inferred];
|
||||
export declare const CatalogFeedSkillEntrySchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
type: "skill";
|
||||
id: string;
|
||||
title: string;
|
||||
version: string;
|
||||
state: "available" | "recommended" | "disabled" | "blocked" | "deprecated";
|
||||
publisher: {
|
||||
id: string;
|
||||
trust: "official" | "community";
|
||||
};
|
||||
install: {
|
||||
candidates: {
|
||||
sourceRef: string;
|
||||
package: string;
|
||||
version: string;
|
||||
integrity: string;
|
||||
}[];
|
||||
};
|
||||
}, {}>;
|
||||
export type CatalogFeedSkillEntry = (typeof CatalogFeedSkillEntrySchema)[inferred];
|
||||
export declare const CatalogFeedEntrySchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
type: "plugin";
|
||||
id: string;
|
||||
@@ -28,6 +68,24 @@ export declare const CatalogFeedEntrySchema: import("arktype/internal/variants/o
|
||||
integrity: string;
|
||||
}[];
|
||||
};
|
||||
} | {
|
||||
type: "skill";
|
||||
id: string;
|
||||
title: string;
|
||||
version: string;
|
||||
state: "available" | "recommended" | "disabled" | "blocked" | "deprecated";
|
||||
publisher: {
|
||||
id: string;
|
||||
trust: "official" | "community";
|
||||
};
|
||||
install: {
|
||||
candidates: {
|
||||
sourceRef: string;
|
||||
package: string;
|
||||
version: string;
|
||||
integrity: string;
|
||||
}[];
|
||||
};
|
||||
}, {}>;
|
||||
export type CatalogFeedEntry = (typeof CatalogFeedEntrySchema)[inferred];
|
||||
export declare const CatalogFeedSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
@@ -36,7 +94,7 @@ export declare const CatalogFeedSchema: import("arktype/internal/variants/object
|
||||
generatedAt: string;
|
||||
sequence: number;
|
||||
expiresAt: string;
|
||||
entries: {
|
||||
entries: ({
|
||||
type: "plugin";
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -54,12 +112,32 @@ export declare const CatalogFeedSchema: import("arktype/internal/variants/object
|
||||
integrity: string;
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
} | {
|
||||
type: "skill";
|
||||
id: string;
|
||||
title: string;
|
||||
version: string;
|
||||
state: "available" | "recommended" | "disabled" | "blocked" | "deprecated";
|
||||
publisher: {
|
||||
id: string;
|
||||
trust: "official" | "community";
|
||||
};
|
||||
install: {
|
||||
candidates: {
|
||||
sourceRef: string;
|
||||
package: string;
|
||||
version: string;
|
||||
integrity: string;
|
||||
}[];
|
||||
};
|
||||
})[];
|
||||
description?: string | undefined;
|
||||
}, {}>;
|
||||
export type CatalogFeed = (typeof CatalogFeedSchema)[inferred];
|
||||
export declare const CATALOG_FEED_SCHEMA_VERSION = 1;
|
||||
export declare const CATALOG_FEED_ID = "clawhub-official";
|
||||
export declare const CATALOG_FEED_SOURCE_REF = "public-clawhub";
|
||||
export declare const CATALOG_SKILLS_FEED_ID = "clawhub-official-skills";
|
||||
export declare const CATALOG_SKILLS_FEED_DESCRIPTION = "Skills published by verified OpenClaw organizations on ClawHub.";
|
||||
export declare function parseCatalogFeed(value: unknown): CatalogFeed;
|
||||
export declare function serializeCatalogFeed(feed: CatalogFeed): string;
|
||||
|
||||
Vendored
+12
-2
@@ -8,9 +8,8 @@ export const CatalogFeedInstallCandidateSchema = type({
|
||||
version: "string",
|
||||
integrity: "string",
|
||||
});
|
||||
export const CatalogFeedEntrySchema = type({
|
||||
const CatalogFeedEntryBaseSchema = {
|
||||
"+": "reject",
|
||||
type: '"plugin"',
|
||||
id: "string",
|
||||
title: "string",
|
||||
version: "string",
|
||||
@@ -24,7 +23,16 @@ export const CatalogFeedEntrySchema = type({
|
||||
"+": "reject",
|
||||
candidates: CatalogFeedInstallCandidateSchema.array(),
|
||||
},
|
||||
};
|
||||
export const CatalogFeedPluginEntrySchema = type({
|
||||
...CatalogFeedEntryBaseSchema,
|
||||
type: '"plugin"',
|
||||
});
|
||||
export const CatalogFeedSkillEntrySchema = type({
|
||||
...CatalogFeedEntryBaseSchema,
|
||||
type: '"skill"',
|
||||
});
|
||||
export const CatalogFeedEntrySchema = type(CatalogFeedPluginEntrySchema.or(CatalogFeedSkillEntrySchema));
|
||||
export const CatalogFeedSchema = type({
|
||||
"+": "reject",
|
||||
schemaVersion: "number",
|
||||
@@ -38,6 +46,8 @@ export const CatalogFeedSchema = type({
|
||||
export const CATALOG_FEED_SCHEMA_VERSION = 1;
|
||||
export const CATALOG_FEED_ID = "clawhub-official";
|
||||
export const CATALOG_FEED_SOURCE_REF = "public-clawhub";
|
||||
export const CATALOG_SKILLS_FEED_ID = "clawhub-official-skills";
|
||||
export const CATALOG_SKILLS_FEED_DESCRIPTION = "Skills published by verified OpenClaw organizations on ClawHub.";
|
||||
export function parseCatalogFeed(value) {
|
||||
const feed = CatalogFeedSchema.assert(value);
|
||||
if (feed.schemaVersion !== CATALOG_FEED_SCHEMA_VERSION) {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"catalogFeed.js","sourceRoot":"","sources":["../src/catalogFeed.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"catalogFeed.js","sourceRoot":"","sources":["../src/catalogFeed.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
@@ -23,6 +23,7 @@ export declare const ApiRoutes: {
|
||||
readonly codePlugins: "/api/v1/code-plugins";
|
||||
readonly bundlePlugins: "/api/v1/bundle-plugins";
|
||||
readonly catalogFeed: "/api/v1/feeds/plugins";
|
||||
readonly catalogSkillsFeed: "/api/v1/feeds/skills";
|
||||
readonly stars: "/api/v1/stars";
|
||||
readonly transfers: "/api/v1/transfers";
|
||||
readonly publishers: "/api/v1/publishers";
|
||||
|
||||
Vendored
+1
@@ -23,6 +23,7 @@ export const ApiRoutes = {
|
||||
codePlugins: "/api/v1/code-plugins",
|
||||
bundlePlugins: "/api/v1/bundle-plugins",
|
||||
catalogFeed: "/api/v1/feeds/plugins",
|
||||
catalogSkillsFeed: "/api/v1/feeds/skills",
|
||||
stars: "/api/v1/stars",
|
||||
transfers: "/api/v1/transfers",
|
||||
publishers: "/api/v1/publishers",
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,iBAAiB;IAC5B,YAAY,EAAE,qBAAqB;IACnC,UAAU,EAAE,kBAAkB;IAC9B,mBAAmB,EAAE,4BAA4B;IACjD,cAAc,EAAE,uBAAuB;IACvC,gBAAgB,EAAE,yBAAyB;CACnC,CAAC;AAEX,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,iBAAiB;IAC1B,QAAQ,EAAE,kBAAkB;IAC5B,gBAAgB,EAAE,4BAA4B;IAC9C,MAAM,EAAE,gBAAgB;IACxB,UAAU,EAAE,uBAAuB;IACnC,OAAO,EAAE,iBAAiB;IAC1B,aAAa,EAAE,wBAAwB;IACvC,QAAQ,EAAE,kBAAkB;IAC5B,WAAW,EAAE,sBAAsB;IACnC,aAAa,EAAE,wBAAwB;IACvC,WAAW,EAAE,uBAAuB;IACpC,KAAK,EAAE,eAAe;IACtB,SAAS,EAAE,mBAAmB;IAC9B,UAAU,EAAE,oBAAoB;IAChC,KAAK,EAAE,eAAe;IACtB,aAAa,EAAE,wBAAwB;IACvC,MAAM,EAAE,gBAAgB;IACxB,YAAY,EAAE,uBAAuB;CAC7B,CAAC"}
|
||||
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,iBAAiB;IAC5B,YAAY,EAAE,qBAAqB;IACnC,UAAU,EAAE,kBAAkB;IAC9B,mBAAmB,EAAE,4BAA4B;IACjD,cAAc,EAAE,uBAAuB;IACvC,gBAAgB,EAAE,yBAAyB;CACnC,CAAC;AAEX,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,iBAAiB;IAC1B,QAAQ,EAAE,kBAAkB;IAC5B,gBAAgB,EAAE,4BAA4B;IAC9C,MAAM,EAAE,gBAAgB;IACxB,UAAU,EAAE,uBAAuB;IACnC,OAAO,EAAE,iBAAiB;IAC1B,aAAa,EAAE,wBAAwB;IACvC,QAAQ,EAAE,kBAAkB;IAC5B,WAAW,EAAE,sBAAsB;IACnC,aAAa,EAAE,wBAAwB;IACvC,WAAW,EAAE,uBAAuB;IACpC,iBAAiB,EAAE,sBAAsB;IACzC,KAAK,EAAE,eAAe;IACtB,SAAS,EAAE,mBAAmB;IAC9B,UAAU,EAAE,oBAAoB;IAChC,KAAK,EAAE,eAAe;IACtB,aAAa,EAAE,wBAAwB;IACvC,MAAM,EAAE,gBAAgB;IACxB,YAAY,EAAE,uBAAuB;CAC7B,CAAC"}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CATALOG_FEED_ID,
|
||||
CATALOG_FEED_SCHEMA_VERSION,
|
||||
CATALOG_FEED_SOURCE_REF,
|
||||
CATALOG_SKILLS_FEED_ID,
|
||||
parseCatalogFeed,
|
||||
serializeCatalogFeed,
|
||||
type CatalogFeed,
|
||||
@@ -115,4 +116,32 @@ describe("catalog feed schema", () => {
|
||||
),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("accepts skill entries in a skills feed", () => {
|
||||
const feed = makeFeed({
|
||||
id: CATALOG_SKILLS_FEED_ID,
|
||||
entries: [
|
||||
{
|
||||
type: "skill",
|
||||
id: "@openclaw/demo",
|
||||
title: "Demo",
|
||||
version: "1.0.0",
|
||||
state: "available",
|
||||
publisher: { id: "openclaw", trust: "official" },
|
||||
install: {
|
||||
candidates: [
|
||||
{
|
||||
sourceRef: CATALOG_FEED_SOURCE_REF,
|
||||
package: "@openclaw/demo",
|
||||
version: "1.0.0",
|
||||
integrity: "sha256:abc",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(parseCatalogFeed(feed).entries[0]?.type).toBe("skill");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,9 +17,8 @@ export const CatalogFeedInstallCandidateSchema = type({
|
||||
});
|
||||
export type CatalogFeedInstallCandidate = (typeof CatalogFeedInstallCandidateSchema)[inferred];
|
||||
|
||||
export const CatalogFeedEntrySchema = type({
|
||||
const CatalogFeedEntryBaseSchema = {
|
||||
"+": "reject",
|
||||
type: '"plugin"',
|
||||
id: "string",
|
||||
title: "string",
|
||||
version: "string",
|
||||
@@ -33,7 +32,23 @@ export const CatalogFeedEntrySchema = type({
|
||||
"+": "reject",
|
||||
candidates: CatalogFeedInstallCandidateSchema.array(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const CatalogFeedPluginEntrySchema = type({
|
||||
...CatalogFeedEntryBaseSchema,
|
||||
type: '"plugin"',
|
||||
});
|
||||
export type CatalogFeedPluginEntry = (typeof CatalogFeedPluginEntrySchema)[inferred];
|
||||
|
||||
export const CatalogFeedSkillEntrySchema = type({
|
||||
...CatalogFeedEntryBaseSchema,
|
||||
type: '"skill"',
|
||||
});
|
||||
export type CatalogFeedSkillEntry = (typeof CatalogFeedSkillEntrySchema)[inferred];
|
||||
|
||||
export const CatalogFeedEntrySchema = type(
|
||||
CatalogFeedPluginEntrySchema.or(CatalogFeedSkillEntrySchema),
|
||||
);
|
||||
export type CatalogFeedEntry = (typeof CatalogFeedEntrySchema)[inferred];
|
||||
|
||||
export const CatalogFeedSchema = type({
|
||||
@@ -51,6 +66,9 @@ export type CatalogFeed = (typeof CatalogFeedSchema)[inferred];
|
||||
export const CATALOG_FEED_SCHEMA_VERSION = 1;
|
||||
export const CATALOG_FEED_ID = "clawhub-official";
|
||||
export const CATALOG_FEED_SOURCE_REF = "public-clawhub";
|
||||
export const CATALOG_SKILLS_FEED_ID = "clawhub-official-skills";
|
||||
export const CATALOG_SKILLS_FEED_DESCRIPTION =
|
||||
"Skills published by verified OpenClaw organizations on ClawHub.";
|
||||
|
||||
export function parseCatalogFeed(value: unknown): CatalogFeed {
|
||||
const feed = CatalogFeedSchema.assert(value);
|
||||
|
||||
@@ -24,6 +24,7 @@ export const ApiRoutes = {
|
||||
codePlugins: "/api/v1/code-plugins",
|
||||
bundlePlugins: "/api/v1/bundle-plugins",
|
||||
catalogFeed: "/api/v1/feeds/plugins",
|
||||
catalogSkillsFeed: "/api/v1/feeds/skills",
|
||||
stars: "/api/v1/stars",
|
||||
transfers: "/api/v1/transfers",
|
||||
publishers: "/api/v1/publishers",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"schemaVersion": 1,
|
||||
"registry": "https://registry.openclaw.ai",
|
||||
"feeds": {
|
||||
"plugins": "https://registry.openclaw.ai/v1/feeds/plugins"
|
||||
"plugins": "https://registry.openclaw.ai/v1/feeds/plugins",
|
||||
"skills": "https://registry.openclaw.ai/v1/feeds/skills"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ User-agent: *
|
||||
Disallow: /api/
|
||||
Disallow: /admin/
|
||||
Allow: /v1/feeds/plugins
|
||||
Allow: /v1/feeds/skills
|
||||
|
||||
+7
-7
@@ -171,18 +171,18 @@ Keep production rewrites and discovery metadata aligned before release.
|
||||
|
||||
### Hosted catalog feed
|
||||
|
||||
Refresh the OpenClaw hosted plugin feed after the production Convex deployment
|
||||
has the catalog projection:
|
||||
Refresh the OpenClaw hosted plugin and skill feeds after the production Convex
|
||||
deployment has the catalog projections:
|
||||
|
||||
```bash
|
||||
gh workflow run publish-catalog-feed.yml --repo openclaw/clawhub --ref main
|
||||
```
|
||||
|
||||
The workflow stores the current feed snapshot in Convex and serves it through
|
||||
`/v1/feeds/plugins` with public edge-cache validators. The unversioned
|
||||
`/feeds/plugins` path redirects to the versioned route. Attach
|
||||
`registry.openclaw.ai` to the same Vercel project before configuring OpenClaw's
|
||||
default feed URL.
|
||||
The workflow stores both current feed snapshots in Convex and serves them
|
||||
through `/v1/feeds/plugins` and `/v1/feeds/skills` with public edge-cache
|
||||
validators. The unversioned `/feeds/plugins` and `/feeds/skills` paths redirect
|
||||
to their versioned routes. Attach `registry.openclaw.ai` to the same Vercel
|
||||
project before configuring OpenClaw's default feed URLs.
|
||||
|
||||
## 5) Post-deploy checks
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ read_when:
|
||||
|
||||
# Hosted Catalog Feed
|
||||
|
||||
ClawHub is the canonical producer for the initial OpenClaw plugin feed. The
|
||||
feed is a projection of the existing public package and release records; it is
|
||||
not a second package catalog.
|
||||
ClawHub is the canonical producer for the initial OpenClaw plugin and skill
|
||||
feeds. The feeds are projections of the existing public package, release, and
|
||||
skill records; they are not second catalogs.
|
||||
|
||||
## Contract
|
||||
|
||||
@@ -36,12 +36,19 @@ feeds stack must add its RFC-entry adapter before `registry.openclaw.ai` is
|
||||
enabled as the default client feed; publishing this snapshot is otherwise
|
||||
safe, but pre-adapter clients will fall back to their bundled catalog.
|
||||
|
||||
The skills feed uses the same envelope and `/v1/feeds/skills` route. It emits
|
||||
`type: "skill"` entries with `@<publisher>/<slug>` ids and ClawHub install
|
||||
coordinates. It includes only skills with an active latest published version,
|
||||
non-empty files, a SHA-256 integrity hash, and an active official publisher
|
||||
whose publisher kind is `org`. Personal publishers and unverified
|
||||
organizations are excluded.
|
||||
|
||||
## Publication
|
||||
|
||||
`convex/catalogFeed.ts` builds the feed from indexed package queries and stores
|
||||
one current publication row in `catalogFeedPublications`. Keeping one row avoids
|
||||
an unbounded publication log while preserving the sequence and exact payload
|
||||
needed for validators.
|
||||
`convex/catalogFeed.ts` builds both feeds from indexed package/skill queries and
|
||||
stores one current publication row per feed in `catalogFeedPublications`.
|
||||
Keeping one row per feed avoids an unbounded publication log while preserving
|
||||
the sequence and exact payload needed for validators.
|
||||
|
||||
The `Publish Hosted Catalog Feed` workflow refreshes the snapshot every six
|
||||
hours and can be run manually. It requires the existing `Production` environment
|
||||
@@ -51,8 +58,8 @@ advertised to OpenClaw clients until the signing key and trust root are deployed
|
||||
|
||||
## Edge delivery
|
||||
|
||||
The HTTP endpoint is `/api/v1/feeds/plugins`. It returns the stored bytes
|
||||
unchanged and provides:
|
||||
The HTTP endpoints are `/api/v1/feeds/plugins` and `/api/v1/feeds/skills`. Each
|
||||
returns its stored bytes unchanged and provides:
|
||||
|
||||
- `ETag: "sha256:<payload hash>"`
|
||||
- `Last-Modified`
|
||||
@@ -60,17 +67,18 @@ unchanged and provides:
|
||||
- `Surrogate-Control: max-age=300, stale-while-revalidate=86400`
|
||||
- `304 Not Modified` for matching `If-None-Match` or `If-Modified-Since`
|
||||
|
||||
`vercel.json` exposes `/v1/feeds/plugins` as an edge-friendly rewrite to the
|
||||
Convex endpoint. The unversioned `/feeds/plugins` path permanently redirects
|
||||
to the versioned path. The `registry.openclaw.ai` custom domain must point at
|
||||
the same Vercel project before the public RFC URL is enabled.
|
||||
`vercel.json` exposes both `/v1/feeds/plugins` and `/v1/feeds/skills` as
|
||||
edge-friendly rewrites to the Convex endpoints. The unversioned `/feeds/*`
|
||||
paths permanently redirect to their versioned paths. The
|
||||
`registry.openclaw.ai` custom domain must point at the same Vercel project
|
||||
before the public RFC URLs are enabled.
|
||||
|
||||
The serialized payload uses stable object-key ordering and deterministic entry
|
||||
and install-candidate ordering. Additive fields may be introduced within a
|
||||
major version; incompatible wire changes require a new versioned route and
|
||||
schema version.
|
||||
|
||||
`/.well-known/openclaw-registry.json` advertises the versioned plugin feed.
|
||||
`/.well-known/openclaw-registry.json` advertises both versioned feeds.
|
||||
`/.well-known/clawhub.json` remains the ClawHub API discovery document.
|
||||
|
||||
Do not make the feed request-time dynamic. Refresh the stored publication first,
|
||||
|
||||
@@ -107,11 +107,26 @@ let highlighterPromise: Promise<unknown> | null = null;
|
||||
|
||||
function loadHighlighter(): Promise<unknown> {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = import("shiki").then(({ createHighlighter }) =>
|
||||
createHighlighter({
|
||||
themes: [...SHIKI_THEMES],
|
||||
langs: SHIKI_LANGS,
|
||||
}),
|
||||
highlighterPromise = Promise.all([
|
||||
import("shiki/dist/core.mjs"),
|
||||
import("shiki/dist/engine-javascript.mjs"),
|
||||
import("shiki/dist/langs.mjs"),
|
||||
import("shiki/dist/themes.mjs"),
|
||||
]).then(
|
||||
([
|
||||
{ createBundledHighlighter },
|
||||
{ createJavaScriptRegexEngine },
|
||||
{ bundledLanguages },
|
||||
{ bundledThemes },
|
||||
]) =>
|
||||
createBundledHighlighter({
|
||||
langs: bundledLanguages,
|
||||
themes: bundledThemes,
|
||||
engine: () => createJavaScriptRegexEngine(),
|
||||
})({
|
||||
themes: [...SHIKI_THEMES],
|
||||
langs: SHIKI_LANGS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return highlighterPromise;
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
"destination": "/v1/feeds/plugins",
|
||||
"statusCode": 308
|
||||
},
|
||||
{
|
||||
"source": "/feeds/skills",
|
||||
"destination": "/v1/feeds/skills",
|
||||
"statusCode": 308
|
||||
},
|
||||
{
|
||||
"source": "/",
|
||||
"has": [
|
||||
@@ -59,6 +64,10 @@
|
||||
{
|
||||
"source": "/v1/feeds/plugins",
|
||||
"destination": "https://wry-manatee-359.convex.site/api/v1/feeds/plugins"
|
||||
},
|
||||
{
|
||||
"source": "/v1/feeds/skills",
|
||||
"destination": "https://wry-manatee-359.convex.site/api/v1/feeds/skills"
|
||||
}
|
||||
],
|
||||
"images": {
|
||||
|
||||
@@ -175,6 +175,9 @@ const config = defineConfig({
|
||||
"convex/browser": convexBrowserPath,
|
||||
"convex/values": convexValuesPath,
|
||||
"@convex-dev/auth/react": convexAuthReactPath,
|
||||
// MarkdownPreview uses Shiki's JavaScript engine; keep rehype from
|
||||
// selecting the WASM-only `shiki/core` export condition.
|
||||
"shiki/core": "shiki/dist/core.mjs",
|
||||
},
|
||||
// Use native Vite tsconfig paths resolution instead of the plugin
|
||||
tsconfigPaths: true,
|
||||
|
||||
Reference in New Issue
Block a user