b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
297 lines
9.1 KiB
TypeScript
297 lines
9.1 KiB
TypeScript
/**
|
|
* Coverage for the manifest's identity (`slug`, `version`) and
|
|
* trust-contract fields (`capabilities`, `allowedHosts`, `storage`).
|
|
*
|
|
* The trust contract is the manifest's most security-sensitive surface.
|
|
* Capability vocabulary, deprecated-name rejection, and the cross-field
|
|
* `network:request` / `allowedHosts` rules all need explicit coverage so
|
|
* a future schema edit can't silently relax the validation.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
AllowedHostsSchema,
|
|
CapabilitiesSchema,
|
|
CapabilitySchema,
|
|
ManifestSchema,
|
|
SlugSchema,
|
|
StorageSchema,
|
|
VersionSchema,
|
|
} from "../src/manifest/schema.js";
|
|
|
|
describe("SlugSchema", () => {
|
|
it("accepts the canonical form", () => {
|
|
expect(SlugSchema.parse("gallery")).toBe("gallery");
|
|
expect(SlugSchema.parse("my-plugin")).toBe("my-plugin");
|
|
expect(SlugSchema.parse("plugin_v2")).toBe("plugin_v2");
|
|
});
|
|
|
|
it("rejects leading digit", () => {
|
|
const result = SlugSchema.safeParse("1-plugin");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects leading punctuation", () => {
|
|
expect(SlugSchema.safeParse("-plugin").success).toBe(false);
|
|
expect(SlugSchema.safeParse("_plugin").success).toBe(false);
|
|
});
|
|
|
|
it("rejects uppercase", () => {
|
|
const result = SlugSchema.safeParse("MyPlugin");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects empty", () => {
|
|
expect(SlugSchema.safeParse("").success).toBe(false);
|
|
});
|
|
|
|
it("rejects over 64 chars", () => {
|
|
const result = SlugSchema.safeParse("a".repeat(65));
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("VersionSchema", () => {
|
|
it("accepts the canonical form", () => {
|
|
expect(VersionSchema.parse("0.1.0")).toBe("0.1.0");
|
|
expect(VersionSchema.parse("1.2.3")).toBe("1.2.3");
|
|
expect(VersionSchema.parse("1.0.0-rc.1")).toBe("1.0.0-rc.1");
|
|
});
|
|
|
|
it("rejects build metadata (atproto rkey constraint)", () => {
|
|
// The atproto record-key alphabet has no `+`, so a semver
|
|
// build-metadata suffix can't survive into the publish path.
|
|
const result = VersionSchema.safeParse("1.0.0+build.1");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects malformed semver", () => {
|
|
expect(VersionSchema.safeParse("1").success).toBe(false);
|
|
expect(VersionSchema.safeParse("1.0").success).toBe(false);
|
|
expect(VersionSchema.safeParse("v1.0.0").success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("CapabilitySchema", () => {
|
|
it("accepts a current capability", () => {
|
|
expect(CapabilitySchema.parse("content:read")).toBe("content:read");
|
|
expect(CapabilitySchema.parse("network:request")).toBe("network:request");
|
|
expect(CapabilitySchema.parse("email:send")).toBe("email:send");
|
|
});
|
|
|
|
it("rejects a deprecated capability with a hint at the replacement", () => {
|
|
const result = CapabilitySchema.safeParse("read:content");
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.issues[0]?.message).toContain("deprecated");
|
|
expect(result.error.issues[0]?.message).toContain("content:read");
|
|
}
|
|
});
|
|
|
|
it("rejects an unknown capability", () => {
|
|
const result = CapabilitySchema.safeParse("filesystem:write");
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.issues[0]?.message).toContain("not a recognised name");
|
|
}
|
|
});
|
|
|
|
it("rejects empty string", () => {
|
|
expect(CapabilitySchema.safeParse("").success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("CapabilitiesSchema (array-level)", () => {
|
|
it("accepts an empty list (no privileges beyond defaults)", () => {
|
|
expect(CapabilitiesSchema.parse([])).toEqual([]);
|
|
});
|
|
|
|
it("rejects more than 32 entries", () => {
|
|
// All entries are valid capabilities but the count alone should
|
|
// trip the array max. Repeat a valid one rather than constructing
|
|
// 33 distinct names that would also each fail individually.
|
|
const result = CapabilitiesSchema.safeParse(Array.from({ length: 33 }).fill("content:read"));
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("AllowedHostsSchema", () => {
|
|
it("accepts hostnames and wildcard-subdomain patterns", () => {
|
|
expect(AllowedHostsSchema.parse(["api.example.com"])).toEqual(["api.example.com"]);
|
|
expect(AllowedHostsSchema.parse(["*.cdn.example.com"])).toEqual(["*.cdn.example.com"]);
|
|
});
|
|
|
|
it("rejects URLs (scheme present)", () => {
|
|
const result = AllowedHostsSchema.safeParse(["https://api.example.com"]);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects host:port", () => {
|
|
// Port must be carried separately; the lexicon's grammar doesn't
|
|
// include ports in the pattern.
|
|
const result = AllowedHostsSchema.safeParse(["api.example.com:8080"]);
|
|
// `:` is allowed-but-we-don't-validate at this layer (no
|
|
// scheme/path/whitespace is the only structural test); a port
|
|
// is technically passes the loose check. Document as intentional
|
|
// — the lexicon's host-pattern grammar is the strict validator.
|
|
// Update this test if we tighten the regex.
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("rejects paths", () => {
|
|
const result = AllowedHostsSchema.safeParse(["api.example.com/some/path"]);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects whitespace", () => {
|
|
const result = AllowedHostsSchema.safeParse(["api.example.com "]);
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("StorageSchema", () => {
|
|
it("accepts a simple single-field index", () => {
|
|
const result = StorageSchema.parse({
|
|
events: { indexes: ["timestamp"] },
|
|
});
|
|
expect(result).toEqual({ events: { indexes: ["timestamp"] } });
|
|
});
|
|
|
|
it("accepts composite indexes", () => {
|
|
const result = StorageSchema.parse({
|
|
events: { indexes: [["collection", "timestamp"]] },
|
|
});
|
|
expect(result.events?.indexes).toEqual([["collection", "timestamp"]]);
|
|
});
|
|
|
|
it("accepts uniqueIndexes alongside indexes", () => {
|
|
const result = StorageSchema.parse({
|
|
users: {
|
|
indexes: ["createdAt"],
|
|
uniqueIndexes: ["email"],
|
|
},
|
|
});
|
|
expect(result.users?.uniqueIndexes).toEqual(["email"]);
|
|
});
|
|
|
|
it("rejects an invalid collection name", () => {
|
|
const result = StorageSchema.safeParse({
|
|
"Bad-Name": { indexes: [] },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects an empty composite index", () => {
|
|
const result = StorageSchema.safeParse({
|
|
events: { indexes: [[]] },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("rejects unknown keys on a collection config", () => {
|
|
const result = StorageSchema.safeParse({
|
|
events: { indexes: [], orderBy: "timestamp" },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("ManifestSchema cross-field rules", () => {
|
|
const base = {
|
|
slug: "my-plugin",
|
|
version: "0.1.0",
|
|
publisher: "example.com",
|
|
license: "MIT",
|
|
author: { name: "Jane Doe" },
|
|
security: { email: "security@example.com" },
|
|
};
|
|
|
|
it("network:request requires non-empty allowedHosts", () => {
|
|
const result = ManifestSchema.safeParse({
|
|
...base,
|
|
capabilities: ["network:request"],
|
|
allowedHosts: [],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(result.error.issues.some((i) => i.message.includes("non-empty `allowedHosts`"))).toBe(
|
|
true,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("network:request with at least one allowed host passes", () => {
|
|
const result = ManifestSchema.safeParse({
|
|
...base,
|
|
capabilities: ["network:request"],
|
|
allowedHosts: ["api.example.com"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("network:request:unrestricted forbids allowedHosts", () => {
|
|
// The lexicon's invariant: allowedHosts MUST NOT appear when
|
|
// unrestricted is declared. The unrestricted capability already
|
|
// grants any host.
|
|
const result = ManifestSchema.safeParse({
|
|
...base,
|
|
capabilities: ["network:request:unrestricted"],
|
|
allowedHosts: ["api.example.com"],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (!result.success) {
|
|
expect(
|
|
result.error.issues.some((i) => i.message.includes("`allowedHosts` must be empty")),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("network:request:unrestricted with empty allowedHosts passes", () => {
|
|
const result = ManifestSchema.safeParse({
|
|
...base,
|
|
capabilities: ["network:request:unrestricted"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("non-network capabilities don't require allowedHosts", () => {
|
|
const result = ManifestSchema.safeParse({
|
|
...base,
|
|
capabilities: ["content:read"],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("ManifestSchema with the trust contract", () => {
|
|
const base = {
|
|
slug: "my-plugin",
|
|
version: "0.1.0",
|
|
publisher: "example.com",
|
|
license: "MIT",
|
|
author: { name: "Jane Doe" },
|
|
security: { email: "security@example.com" },
|
|
};
|
|
|
|
it("defaults capabilities/allowedHosts/storage to empty when omitted", () => {
|
|
const result = ManifestSchema.parse(base);
|
|
expect(result.capabilities).toEqual([]);
|
|
expect(result.allowedHosts).toEqual([]);
|
|
expect(result.storage).toEqual({});
|
|
});
|
|
|
|
it("accepts a full trust contract", () => {
|
|
const result = ManifestSchema.parse({
|
|
...base,
|
|
capabilities: ["content:read", "content:write", "network:request"],
|
|
allowedHosts: ["api.example.com", "*.cdn.example.com"],
|
|
storage: {
|
|
events: { indexes: ["timestamp"] },
|
|
users: { indexes: ["createdAt"], uniqueIndexes: ["email"] },
|
|
},
|
|
});
|
|
expect(result.capabilities).toEqual(["content:read", "content:write", "network:request"]);
|
|
});
|
|
});
|