mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
fix(catalog): version and harden hosted plugin feed (#2806)
This commit is contained in:
@@ -37,7 +37,7 @@ jobs:
|
||||
needs: validate-ref
|
||||
environment:
|
||||
name: Production
|
||||
url: https://registry.openclaw.ai/feeds/plugins
|
||||
url: https://registry.openclaw.ai/v1/feeds/plugins
|
||||
env:
|
||||
EXPIRES_IN_DAYS: ${{ inputs.expires_in_days || '7' }}
|
||||
steps:
|
||||
|
||||
@@ -50,6 +50,37 @@ describe("catalogFeedV1Handler", () => {
|
||||
expect(await response.text()).toBe("");
|
||||
});
|
||||
|
||||
it("accepts weak etags and last-modified validators", async () => {
|
||||
const weakEtagResponse = await catalogFeedV1Handler(
|
||||
ctx as never,
|
||||
new Request("https://clawhub.ai/feed", {
|
||||
headers: { "If-None-Match": 'W/"sha256:abc123"' },
|
||||
}),
|
||||
);
|
||||
expect(weakEtagResponse.status).toBe(304);
|
||||
|
||||
const lastModifiedResponse = await catalogFeedV1Handler(
|
||||
ctx as never,
|
||||
new Request("https://clawhub.ai/feed", {
|
||||
headers: { "If-Modified-Since": "Tue, 23 Jun 2026 00:00:00 GMT" },
|
||||
}),
|
||||
);
|
||||
expect(lastModifiedResponse.status).toBe(304);
|
||||
});
|
||||
|
||||
it("gives etag precedence over last-modified", async () => {
|
||||
const response = await catalogFeedV1Handler(
|
||||
ctx as never,
|
||||
new Request("https://clawhub.ai/feed", {
|
||||
headers: {
|
||||
"If-None-Match": '"sha256:different"',
|
||||
"If-Modified-Since": "Tue, 23 Jun 2026 00:00:00 GMT",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it("does not cache an unpublished feed", async () => {
|
||||
ctx.runQuery.mockResolvedValue(null);
|
||||
const response = await catalogFeedV1Handler(
|
||||
|
||||
@@ -8,7 +8,19 @@ function matchesEtag(request: Request, etag: string) {
|
||||
return header
|
||||
.split(",")
|
||||
.map((value) => value.trim())
|
||||
.some((value) => value === "*" || value === etag);
|
||||
.some((value) => {
|
||||
if (value === "*") return true;
|
||||
const normalized = value.startsWith("W/") ? value.slice(2) : value;
|
||||
return normalized === etag;
|
||||
});
|
||||
}
|
||||
|
||||
function matchesLastModified(request: Request, publishedAt: number) {
|
||||
const header = request.headers.get("if-modified-since");
|
||||
if (!header) return false;
|
||||
const since = Date.parse(header);
|
||||
if (!Number.isFinite(since)) return false;
|
||||
return Math.floor(publishedAt / 1000) * 1000 <= since;
|
||||
}
|
||||
|
||||
export async function catalogFeedV1Handler(ctx: ActionCtx, request: Request) {
|
||||
@@ -42,6 +54,11 @@ export async function catalogFeedV1Handler(ctx: ActionCtx, request: Request) {
|
||||
corsHeaders(),
|
||||
);
|
||||
|
||||
if (matchesEtag(request, etag)) return new Response(null, { status: 304, headers });
|
||||
if (
|
||||
matchesEtag(request, etag) ||
|
||||
(!request.headers.has("if-none-match") && matchesLastModified(request, publication.publishedAt))
|
||||
) {
|
||||
return new Response(null, { status: 304, headers });
|
||||
}
|
||||
return new Response(publication.payload, { status: 200, headers });
|
||||
}
|
||||
|
||||
Vendored
+34
-2
@@ -57,7 +57,39 @@ export function parseCatalogFeed(value) {
|
||||
}
|
||||
export function serializeCatalogFeed(feed) {
|
||||
const parsed = parseCatalogFeed(feed);
|
||||
const entries = [...parsed.entries].sort((left, right) => left.id.localeCompare(right.id));
|
||||
return JSON.stringify({ ...parsed, entries });
|
||||
const entries = [...parsed.entries]
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
.map((entry) => ({
|
||||
type: entry.type,
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
version: entry.version,
|
||||
state: entry.state,
|
||||
publisher: {
|
||||
id: entry.publisher.id,
|
||||
trust: entry.publisher.trust,
|
||||
},
|
||||
install: {
|
||||
candidates: [...entry.install.candidates]
|
||||
.sort((left, right) => [left.sourceRef, left.package, left.version, left.integrity]
|
||||
.join("\u0000")
|
||||
.localeCompare([right.sourceRef, right.package, right.version, right.integrity].join("\u0000")))
|
||||
.map((candidate) => ({
|
||||
sourceRef: candidate.sourceRef,
|
||||
package: candidate.package,
|
||||
version: candidate.version,
|
||||
integrity: candidate.integrity,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
return JSON.stringify({
|
||||
schemaVersion: parsed.schemaVersion,
|
||||
id: parsed.id,
|
||||
generatedAt: parsed.generatedAt,
|
||||
sequence: parsed.sequence,
|
||||
expiresAt: parsed.expiresAt,
|
||||
...(parsed.description === undefined ? {} : { description: parsed.description }),
|
||||
entries,
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=catalogFeed.js.map
|
||||
+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":"AAAA,OAAO,EAAiB,IAAI,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CACxC,6DAA6D,CAC9D,CAAC;AAGF,MAAM,CAAC,MAAM,+BAA+B,GAAG,IAAI,CAAC,wBAAwB,CAAC,CAAC;AAG9E,MAAM,CAAC,MAAM,iCAAiC,GAAG,IAAI,CAAC;IACpD,GAAG,EAAE,QAAQ;IACb,SAAS,EAAE,QAAQ;IACnB,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,QAAQ;IACjB,SAAS,EAAE,QAAQ;CACpB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;IACzC,GAAG,EAAE,QAAQ;IACb,IAAI,EAAE,UAAU;IAChB,EAAE,EAAE,QAAQ;IACZ,KAAK,EAAE,QAAQ;IACf,OAAO,EAAE,QAAQ;IACjB,KAAK,EAAE,sBAAsB;IAC7B,SAAS,EAAE;QACT,GAAG,EAAE,QAAQ;QACb,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,+BAA+B;KACvC;IACD,OAAO,EAAE;QACP,GAAG,EAAE,QAAQ;QACb,UAAU,EAAE,iCAAiC,CAAC,KAAK,EAAE;KACtD;CACF,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACpC,GAAG,EAAE,QAAQ;IACb,aAAa,EAAE,QAAQ;IACvB,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,QAAQ;IACnB,WAAW,EAAE,SAAS;IACtB,OAAO,EAAE,sBAAsB,CAAC,KAAK,EAAE;CACxC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAClD,MAAM,CAAC,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAExD,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,aAAa,KAAK,2BAA2B,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,4CAA4C,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IACD,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC9C,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAC5C,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAiB;IACpD,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC;SAChC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;SACtD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACf,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,SAAS,EAAE;YACT,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE;YACtB,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,KAAK;SAC7B;QACD,OAAO,EAAE;YACP,UAAU,EAAE,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;iBACtC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACpB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;iBACzD,IAAI,CAAC,QAAQ,CAAC;iBACd,aAAa,CACZ,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAChF,CACJ;iBACA,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBACnB,SAAS,EAAE,SAAS,CAAC,SAAS;gBAC9B,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,OAAO,EAAE,SAAS,CAAC,OAAO;gBAC1B,SAAS,EAAE,SAAS,CAAC,SAAS;aAC/B,CAAC,CAAC;SACN;KACF,CAAC,CAAC,CAAC;IACN,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;QAChF,OAAO;KACR,CAAC,CAAC;AACL,CAAC"}
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
CATALOG_FEED_SOURCE_REF,
|
||||
parseCatalogFeed,
|
||||
serializeCatalogFeed,
|
||||
type CatalogFeed,
|
||||
} from "./catalogFeed.js";
|
||||
|
||||
function makeFeed(overrides: Record<string, unknown> = {}) {
|
||||
function makeFeed(overrides: Partial<CatalogFeed> = {}): CatalogFeed {
|
||||
return {
|
||||
schemaVersion: CATALOG_FEED_SCHEMA_VERSION,
|
||||
id: CATALOG_FEED_ID,
|
||||
@@ -58,12 +59,41 @@ function makeFeed(overrides: Record<string, unknown> = {}) {
|
||||
|
||||
describe("catalog feed schema", () => {
|
||||
it("sorts entries by stable id before serializing", () => {
|
||||
const serialized = serializeCatalogFeed(makeFeed() as never);
|
||||
const serialized = serializeCatalogFeed(makeFeed());
|
||||
expect(serialized.indexOf('"id":"alpha"')).toBeLessThan(serialized.indexOf('"id":"zeta"'));
|
||||
});
|
||||
|
||||
it("serializes equivalent objects to identical canonical bytes", () => {
|
||||
const feed = makeFeed();
|
||||
const reordered: CatalogFeed = {
|
||||
entries: feed.entries.map((entry) => ({
|
||||
install: {
|
||||
candidates: entry.install.candidates.map((candidate) => ({
|
||||
integrity: candidate.integrity,
|
||||
version: candidate.version,
|
||||
package: candidate.package,
|
||||
sourceRef: candidate.sourceRef,
|
||||
})),
|
||||
},
|
||||
publisher: entry.publisher,
|
||||
state: entry.state,
|
||||
version: entry.version,
|
||||
title: entry.title,
|
||||
id: entry.id,
|
||||
type: entry.type,
|
||||
})),
|
||||
expiresAt: feed.expiresAt,
|
||||
sequence: feed.sequence,
|
||||
generatedAt: feed.generatedAt,
|
||||
id: feed.id,
|
||||
schemaVersion: feed.schemaVersion,
|
||||
};
|
||||
|
||||
expect(serializeCatalogFeed(feed)).toBe(serializeCatalogFeed(reordered));
|
||||
});
|
||||
|
||||
it("rejects unsupported versions and expired feeds", () => {
|
||||
expect(() => parseCatalogFeed(makeFeed({ schemaVersion: 2 }))).toThrow(
|
||||
expect(() => parseCatalogFeed(makeFeed({ schemaVersion: 2 } as never))).toThrow(
|
||||
"Unsupported catalog feed schema version",
|
||||
);
|
||||
expect(() => parseCatalogFeed(makeFeed({ expiresAt: "2026-06-22T00:00:00.000Z" }))).toThrow(
|
||||
@@ -81,7 +111,7 @@ describe("catalog feed schema", () => {
|
||||
install: { candidates: [{ sourceRef: CATALOG_FEED_SOURCE_REF }] },
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as never),
|
||||
),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
@@ -74,6 +74,42 @@ export function parseCatalogFeed(value: unknown): CatalogFeed {
|
||||
|
||||
export function serializeCatalogFeed(feed: CatalogFeed): string {
|
||||
const parsed = parseCatalogFeed(feed);
|
||||
const entries = [...parsed.entries].sort((left, right) => left.id.localeCompare(right.id));
|
||||
return JSON.stringify({ ...parsed, entries });
|
||||
const entries = [...parsed.entries]
|
||||
.sort((left, right) => left.id.localeCompare(right.id))
|
||||
.map((entry) => ({
|
||||
type: entry.type,
|
||||
id: entry.id,
|
||||
title: entry.title,
|
||||
version: entry.version,
|
||||
state: entry.state,
|
||||
publisher: {
|
||||
id: entry.publisher.id,
|
||||
trust: entry.publisher.trust,
|
||||
},
|
||||
install: {
|
||||
candidates: [...entry.install.candidates]
|
||||
.sort((left, right) =>
|
||||
[left.sourceRef, left.package, left.version, left.integrity]
|
||||
.join("\u0000")
|
||||
.localeCompare(
|
||||
[right.sourceRef, right.package, right.version, right.integrity].join("\u0000"),
|
||||
),
|
||||
)
|
||||
.map((candidate) => ({
|
||||
sourceRef: candidate.sourceRef,
|
||||
package: candidate.package,
|
||||
version: candidate.version,
|
||||
integrity: candidate.integrity,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
return JSON.stringify({
|
||||
schemaVersion: parsed.schemaVersion,
|
||||
id: parsed.id,
|
||||
generatedAt: parsed.generatedAt,
|
||||
sequence: parsed.sequence,
|
||||
expiresAt: parsed.expiresAt,
|
||||
...(parsed.description === undefined ? {} : { description: parsed.description }),
|
||||
entries,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "openclaw.registry",
|
||||
"schemaVersion": 1,
|
||||
"registry": "https://registry.openclaw.ai",
|
||||
"feeds": {
|
||||
"plugins": "https://registry.openclaw.ai/v1/feeds/plugins"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
Contact: mailto:security@openclaw.ai
|
||||
Expires: 2027-06-23T00:00:00.000Z
|
||||
Preferred-Languages: en
|
||||
Canonical: https://clawhub.ai/.well-known/security.txt
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow: /api/
|
||||
Disallow: /admin/
|
||||
Allow: /v1/feeds/plugins
|
||||
|
||||
+2
-1
@@ -179,7 +179,8 @@ 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
|
||||
`/feeds/plugins` with public edge-cache validators. Attach
|
||||
`/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.
|
||||
|
||||
|
||||
@@ -58,11 +58,20 @@ unchanged and provides:
|
||||
- `Last-Modified`
|
||||
- `Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400`
|
||||
- `Surrogate-Control: max-age=300, stale-while-revalidate=86400`
|
||||
- `304 Not Modified` for matching `If-None-Match`
|
||||
- `304 Not Modified` for matching `If-None-Match` or `If-Modified-Since`
|
||||
|
||||
`vercel.json` exposes `/feeds/plugins` as an edge-friendly rewrite to the
|
||||
Convex endpoint. The `registry.openclaw.ai` custom domain must point at the
|
||||
same Vercel project before the public RFC URL is enabled.
|
||||
`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.
|
||||
|
||||
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/clawhub.json` remains the ClawHub API discovery document.
|
||||
|
||||
Do not make the feed request-time dynamic. Refresh the stored publication first,
|
||||
then let Vercel or the configured CDN cache the immutable response by ETag.
|
||||
|
||||
+6
-1
@@ -21,6 +21,11 @@
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/feeds/plugins",
|
||||
"destination": "/v1/feeds/plugins",
|
||||
"statusCode": 308
|
||||
},
|
||||
{
|
||||
"source": "/",
|
||||
"has": [
|
||||
@@ -52,7 +57,7 @@
|
||||
"destination": "https://wry-manatee-359.convex.site/api/:path*"
|
||||
},
|
||||
{
|
||||
"source": "/feeds/plugins",
|
||||
"source": "/v1/feeds/plugins",
|
||||
"destination": "https://wry-manatee-359.convex.site/api/v1/feeds/plugins"
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user