feat: declare module permissions
Establishes the one canonical PermissionDeclaration contract (packages/contracts/src/authorization.ts) — id/baseline/description, strict, unknown-property-rejecting — consumed by both Core's authorization registry and module manifests, replacing Core's previously separate interface. Bumps the canonical module manifest to manifestVersion 3: permissions become complete declarations instead of bare identifier strings, with manifest-owned namespace and duplicate-id enforcement. Version 2 and bare string-array permissions are rejected outright; there is no compatibility adapter. Adds composePlatformPermissionRegistry (core/src/modules/permissions.ts), a pure, synchronous composer that builds one platform permission registry from Core's own declarations plus every already-discovered module's, in deterministic order, reusing createPermissionRegistry and rejecting any module-contributed permission in the reserved core.* namespace. core/src/authorization remains independent of core/src/modules; only the module integration layer depends on authorization. Updates the module template, module-development docs, authorization docs, and existing manifest fixtures for manifestVersion 3. Does not wire the combined registry into apps/api — that is pass 2.
This commit is contained in:
@@ -31,7 +31,7 @@ async function createModulesDirectory(): Promise<string> {
|
||||
|
||||
function manifestFor(id: string): Record<string, unknown> {
|
||||
return {
|
||||
manifestVersion: 2,
|
||||
manifestVersion: 3,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
@@ -40,7 +40,9 @@ function manifestFor(id: string): Record<string, unknown> {
|
||||
moduleSdk: "^0.1.0",
|
||||
},
|
||||
capabilities: [{ id: `${id}.records`, version: "1.0.0" }],
|
||||
permissions: [`${id}.records.read`],
|
||||
permissions: [
|
||||
{ id: `${id}.records.read`, baseline: "restricted", description: "Read records." },
|
||||
],
|
||||
entrypoints: {
|
||||
server: "./dist/server/index.js",
|
||||
},
|
||||
@@ -151,6 +153,16 @@ describe("module discovery", () => {
|
||||
expect(Object.isFrozen(modules[1]?.manifest)).toBe(true);
|
||||
const mutableBuckets = modules[1]?.manifest.storage.buckets as unknown as Array<{ id: string }>;
|
||||
expect(() => mutableBuckets.push({ id: "tampered" })).toThrow(TypeError);
|
||||
|
||||
expect(modules[1]?.manifest.permissions).toEqual([
|
||||
{ id: "calendar.records.read", baseline: "restricted", description: "Read records." },
|
||||
]);
|
||||
expect(Object.isFrozen(modules[1]?.manifest.permissions)).toBe(true);
|
||||
expect(Object.isFrozen(modules[1]?.manifest.permissions[0])).toBe(true);
|
||||
const mutablePermissions = modules[1]?.manifest.permissions as unknown as Array<{
|
||||
id: string;
|
||||
}>;
|
||||
expect(() => mutablePermissions.push({ id: "tampered" })).toThrow(TypeError);
|
||||
});
|
||||
|
||||
it("aggregates malformed packages and manifests without partial discovery", async () => {
|
||||
@@ -215,7 +227,9 @@ describe("module discovery", () => {
|
||||
...manifestFor("one"),
|
||||
id: "shared",
|
||||
capabilities: [{ id: "shared.records", version: "1.0.0" }],
|
||||
permissions: ["shared.records.read"],
|
||||
permissions: [
|
||||
{ id: "shared.records.read", baseline: "restricted", description: "Read records." },
|
||||
],
|
||||
},
|
||||
packageJson: {
|
||||
name: "@test/duplicate",
|
||||
@@ -229,7 +243,9 @@ describe("module discovery", () => {
|
||||
...manifestFor("two"),
|
||||
id: "shared",
|
||||
capabilities: [{ id: "shared.records", version: "1.0.0" }],
|
||||
permissions: ["shared.records.read"],
|
||||
permissions: [
|
||||
{ id: "shared.records.read", baseline: "restricted", description: "Read records." },
|
||||
],
|
||||
},
|
||||
packageJson: {
|
||||
name: "@test/duplicate",
|
||||
|
||||
Reference in New Issue
Block a user