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:
@@ -1,25 +1,18 @@
|
||||
import {
|
||||
type PermissionBaseline,
|
||||
type PermissionDeclaration,
|
||||
type PermissionId,
|
||||
parsePermissionId,
|
||||
safeParsePermissionId,
|
||||
} from "@ekklesia/contracts";
|
||||
|
||||
/**
|
||||
* A registered permission declaration: the trusted pairing of a permission identifier with its
|
||||
* one baseline. This is what distinguishes a *registered* permission from a merely well-formed
|
||||
* `PermissionId` string — the evaluator itself (`createAuthorizationEvaluator`) still takes an
|
||||
* explicit, caller-supplied baseline per pass 1's design; the registry is what a trusted caller
|
||||
* (Core's own HTTP enforcement, eventually a module registration mechanism) consults so that
|
||||
* baseline is never client-supplied or guessed.
|
||||
* Core's registry consumes the one shared `PermissionDeclaration` shape
|
||||
* (`packages/contracts/src/authorization.ts`) rather than declaring its own interface — a module
|
||||
* manifest's `permissions` array parses into the exact same type, so a declaration a module ships
|
||||
* and a declaration Core registers are provably interchangeable. Re-exported here so existing
|
||||
* `@ekklesia/core/authorization` consumers keep importing it from this package.
|
||||
*/
|
||||
export interface PermissionDeclaration {
|
||||
readonly id: PermissionId;
|
||||
readonly baseline: PermissionBaseline;
|
||||
/** Human-readable, for operators and audit review only — never interpreted by the evaluator or
|
||||
* by enforcement. */
|
||||
readonly description: string;
|
||||
}
|
||||
export type { PermissionDeclaration, PermissionDeclarationInput } from "@ekklesia/contracts";
|
||||
|
||||
export interface PermissionRegistry {
|
||||
readonly declarations: readonly PermissionDeclaration[];
|
||||
@@ -89,7 +82,14 @@ export const CORE_AUTHORIZATION_GRANTS_MANAGE: PermissionId = parsePermissionId(
|
||||
"core.authorization.grants.manage",
|
||||
);
|
||||
|
||||
const CORE_PERMISSION_DECLARATIONS: readonly PermissionDeclaration[] = Object.freeze([
|
||||
/**
|
||||
* Core's own declarations, exported in deeply immutable form so a trusted composer (the module
|
||||
* integration layer, `core/src/modules/permissions.ts`) can build one combined registry from
|
||||
* exactly these plus every discovered module's declarations, in a fixed, deterministic order,
|
||||
* without reconstructing or guessing Core's own list. `CORE_PERMISSION_REGISTRY` below remains the
|
||||
* registry Core-only code and existing tests use directly.
|
||||
*/
|
||||
export const CORE_PERMISSION_DECLARATIONS: readonly PermissionDeclaration[] = Object.freeze([
|
||||
Object.freeze({
|
||||
baseline: "restricted" as const,
|
||||
description: "Create, list, and revoke another user's explicit authorization grants.",
|
||||
|
||||
@@ -47,13 +47,19 @@ export interface ResolvedModuleEntrypoints {
|
||||
readonly jobs?: string;
|
||||
}
|
||||
|
||||
type DeepReadonly<T> = T extends (...arguments_: never[]) => unknown
|
||||
// Branded primitives (`PermissionId`, `ModuleId`, ...) are `string & {brand}` intersection types,
|
||||
// which structurally satisfy `T extends object` — without this check first, the mapped-type
|
||||
// branch below would incorrectly expand a branded string into `String.prototype`'s member shape
|
||||
// and lose the brand entirely, rather than leaving an already-immutable primitive untouched.
|
||||
type DeepReadonly<T> = T extends string | number | boolean | bigint | symbol | null | undefined
|
||||
? T
|
||||
: T extends readonly (infer Item)[]
|
||||
? readonly DeepReadonly<Item>[]
|
||||
: T extends object
|
||||
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
|
||||
: T;
|
||||
: T extends (...arguments_: never[]) => unknown
|
||||
? T
|
||||
: T extends readonly (infer Item)[]
|
||||
? readonly DeepReadonly<Item>[]
|
||||
: T extends object
|
||||
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
|
||||
: T;
|
||||
|
||||
export type ReadonlyModuleManifest = DeepReadonly<ModuleManifest>;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./discovery.js";
|
||||
export * from "./migration-discovery.js";
|
||||
export * from "./permissions.js";
|
||||
export * from "./runtime.js";
|
||||
export * from "./schema-discovery.js";
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
CORE_PERMISSION_DECLARATIONS,
|
||||
createPermissionRegistry,
|
||||
type PermissionDeclaration,
|
||||
type PermissionRegistry,
|
||||
} from "../authorization/index.js";
|
||||
import type { DiscoveredModule } from "./discovery.js";
|
||||
|
||||
const CORE_PERMISSION_NAMESPACE_PREFIX = "core.";
|
||||
|
||||
export type ModulePermissionCompositionErrorCode = "MODULE_PERMISSION_RESERVED_NAMESPACE";
|
||||
|
||||
/** Thrown only at composition time, never per-request. Carries the offending module and
|
||||
* permission id deliberately — like `PermissionRegistryError`, this is a startup-time
|
||||
* configuration error, not a runtime response, and neither value is sensitive. Never carries a
|
||||
* module directory, manifest path, caught error, or stack detail. */
|
||||
export class ModulePermissionCompositionError extends Error {
|
||||
readonly code: ModulePermissionCompositionErrorCode;
|
||||
readonly moduleId: string;
|
||||
readonly permissionId: string;
|
||||
|
||||
constructor(moduleId: string, permissionId: string) {
|
||||
super(
|
||||
`Module "${moduleId}" cannot declare permission "${permissionId}": the "core." namespace is reserved for Core's own permissions.`,
|
||||
);
|
||||
this.name = "ModulePermissionCompositionError";
|
||||
this.code = "MODULE_PERMISSION_RESERVED_NAMESPACE";
|
||||
this.moduleId = moduleId;
|
||||
this.permissionId = permissionId;
|
||||
this.stack = `${this.name}: ${this.message}`;
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core's module-integration-layer composition helper: builds the one platform permission
|
||||
* registry from Core's own declarations (`CORE_PERMISSION_DECLARATIONS`) plus every already
|
||||
* discovered module's manifest-declared permissions. Deliberately lives in `core/src/modules`,
|
||||
* not `core/src/authorization` — the dependency runs one way only, this layer depends on
|
||||
* authorization's `createPermissionRegistry`, and authorization never imports module discovery
|
||||
* or runtime. This is pure, synchronous, in-memory composition over already-discovered,
|
||||
* already-validated data: no filesystem, environment, database, network, or module-code
|
||||
* execution happens here, and the caller (not this function) is responsible for having already
|
||||
* produced a complete, successfully discovered module list (for example via `discoverModules`).
|
||||
*
|
||||
* Declaration order is deterministic and meaningful for duplicate detection: Core's own
|
||||
* declarations first, then each module in the exact order it appears in `modules`, and within a
|
||||
* module its declarations in manifest-array order. Duplicate exact permission ids — including a
|
||||
* module colliding with a Core-owned id or with another module's — fail via the same
|
||||
* `PermissionRegistryError` `createPermissionRegistry` already throws for any duplicate; this
|
||||
* function does not reimplement or duplicate that detection.
|
||||
*
|
||||
* `core.*` is reserved for Core's own permissions. A discovered module contributing any
|
||||
* `core.`-prefixed permission id — regardless of that module's own id, and regardless of whether
|
||||
* it happens to exactly collide with an existing declaration — fails composition with
|
||||
* `ModulePermissionCompositionError` rather than silently registering or silently colliding.
|
||||
*/
|
||||
export function composePlatformPermissionRegistry(
|
||||
modules: readonly DiscoveredModule[],
|
||||
): PermissionRegistry {
|
||||
const declarations: PermissionDeclaration[] = [...CORE_PERMISSION_DECLARATIONS];
|
||||
|
||||
for (const module of modules) {
|
||||
for (const declaration of module.manifest.permissions) {
|
||||
if (declaration.id.startsWith(CORE_PERMISSION_NAMESPACE_PREFIX)) {
|
||||
throw new ModulePermissionCompositionError(module.manifest.id, declaration.id);
|
||||
}
|
||||
declarations.push(declaration);
|
||||
}
|
||||
}
|
||||
|
||||
return createPermissionRegistry(declarations);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { parsePermissionId } from "@ekklesia/contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CORE_AUTHORIZATION_GRANTS_MANAGE,
|
||||
CORE_PERMISSION_DECLARATIONS,
|
||||
CORE_PERMISSION_REGISTRY,
|
||||
createPermissionRegistry,
|
||||
PermissionRegistryError,
|
||||
@@ -92,3 +93,21 @@ describe("CORE_PERMISSION_REGISTRY", () => {
|
||||
expect(CORE_PERMISSION_REGISTRY.declarations).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CORE_PERMISSION_DECLARATIONS", () => {
|
||||
it("is Core's own declaration list, exported separately from the registry in deeply immutable form", () => {
|
||||
expect(CORE_PERMISSION_DECLARATIONS).toEqual([
|
||||
{
|
||||
baseline: "restricted",
|
||||
description: "Create, list, and revoke another user's explicit authorization grants.",
|
||||
id: CORE_AUTHORIZATION_GRANTS_MANAGE,
|
||||
},
|
||||
]);
|
||||
expect(Object.isFrozen(CORE_PERMISSION_DECLARATIONS)).toBe(true);
|
||||
expect(Object.isFrozen(CORE_PERMISSION_DECLARATIONS[0])).toBe(true);
|
||||
});
|
||||
|
||||
it("matches exactly what CORE_PERMISSION_REGISTRY exposes as its own declarations, so a composer building on top of it never diverges from what Core itself registers", () => {
|
||||
expect(CORE_PERMISSION_DECLARATIONS).toEqual(CORE_PERMISSION_REGISTRY.declarations);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ async function createModule(
|
||||
writeFile(
|
||||
path.join(moduleDirectory, "ekklesia.module.json"),
|
||||
JSON.stringify({
|
||||
manifestVersion: 2,
|
||||
manifestVersion: 3,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
|
||||
@@ -39,7 +39,7 @@ function discoveredModule(id: string, version = "0.1.0"): DiscoveredModule {
|
||||
},
|
||||
entrypoints: { server: "./dist/server.js" },
|
||||
id,
|
||||
manifestVersion: 2,
|
||||
manifestVersion: 3,
|
||||
name: `${id} module`,
|
||||
permissions: [],
|
||||
storage: { buckets: [] },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -65,7 +65,7 @@ function manifestFor(
|
||||
} = {},
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
manifestVersion: 2,
|
||||
manifestVersion: 3,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { type PermissionBaseline, parseModuleManifest } from "@ekklesia/contracts";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CORE_AUTHORIZATION_GRANTS_MANAGE,
|
||||
CORE_PERMISSION_DECLARATIONS,
|
||||
PermissionRegistryError,
|
||||
} from "../src/authorization/index.js";
|
||||
import type { DiscoveredModule } from "../src/modules/discovery.js";
|
||||
import {
|
||||
composePlatformPermissionRegistry,
|
||||
ModulePermissionCompositionError,
|
||||
} from "../src/modules/permissions.js";
|
||||
|
||||
interface PermissionInput {
|
||||
readonly id: string;
|
||||
readonly baseline: PermissionBaseline;
|
||||
readonly description: string;
|
||||
}
|
||||
|
||||
/** A minimal, in-memory `DiscoveredModule` double — no filesystem, no real discovery. The manifest
|
||||
* is still built through `parseModuleManifest`, so a fixture that would not actually pass manifest
|
||||
* validation cannot silently misrepresent what real discovery would ever produce. Only `manifest`
|
||||
* is read by `composePlatformPermissionRegistry`; the rest exist only to satisfy the type. */
|
||||
function fakeModule(id: string, permissions: readonly PermissionInput[] = []): DiscoveredModule {
|
||||
const manifest = parseModuleManifest({
|
||||
manifestVersion: 3,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
compatibility: { platform: "^0.1.0", moduleSdk: "^0.1.0" },
|
||||
permissions,
|
||||
entrypoints: {
|
||||
// Deliberately a path nothing will ever resolve, proving composition never imports or
|
||||
// otherwise touches a module's server entrypoint.
|
||||
server: "./dist/does-not-exist/index.js",
|
||||
},
|
||||
});
|
||||
return {
|
||||
directory: `/modules/${id}`,
|
||||
entrypoints: { server: `/modules/${id}/dist/does-not-exist/index.js` },
|
||||
manifest,
|
||||
manifestPath: `/modules/${id}/ekklesia.module.json`,
|
||||
packageName: `@test/${id}`,
|
||||
};
|
||||
}
|
||||
|
||||
describe("composePlatformPermissionRegistry", () => {
|
||||
it("composes a Core-only registry when no modules are supplied", () => {
|
||||
const registry = composePlatformPermissionRegistry([]);
|
||||
|
||||
expect(registry.declarations).toEqual(CORE_PERMISSION_DECLARATIONS);
|
||||
expect(registry.has(CORE_AUTHORIZATION_GRANTS_MANAGE)).toBe(true);
|
||||
});
|
||||
|
||||
it("composes Core and multiple discovered modules' declarations into one registry", () => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
]);
|
||||
const people = fakeModule("people", [
|
||||
{ id: "people.directory.read", baseline: "authenticated", description: "Read directory." },
|
||||
]);
|
||||
|
||||
const registry = composePlatformPermissionRegistry([calendar, people]);
|
||||
|
||||
expect(registry.has(CORE_AUTHORIZATION_GRANTS_MANAGE)).toBe(true);
|
||||
expect(registry.has("calendar.events.read")).toBe(true);
|
||||
expect(registry.has("people.directory.read")).toBe(true);
|
||||
expect(registry.declarations).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("preserves deterministic order: Core first, then modules in the supplied order, then declarations in manifest order", () => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
{ id: "calendar.events.write", baseline: "restricted", description: "Write events." },
|
||||
]);
|
||||
const people = fakeModule("people", [
|
||||
{ id: "people.directory.read", baseline: "authenticated", description: "Read directory." },
|
||||
]);
|
||||
|
||||
const registry = composePlatformPermissionRegistry([calendar, people]);
|
||||
|
||||
expect(registry.declarations.map((declaration) => declaration.id)).toEqual([
|
||||
CORE_AUTHORIZATION_GRANTS_MANAGE,
|
||||
"calendar.events.read",
|
||||
"calendar.events.write",
|
||||
"people.directory.read",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"public",
|
||||
"authenticated",
|
||||
"restricted",
|
||||
] as const)("preserves the exact %s baseline and description of a module declaration", (baseline) => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline, description: "Read calendar events." },
|
||||
]);
|
||||
|
||||
const registry = composePlatformPermissionRegistry([calendar]);
|
||||
|
||||
expect(registry.get("calendar.events.read")).toEqual({
|
||||
id: "calendar.events.read",
|
||||
baseline,
|
||||
description: "Read calendar events.",
|
||||
});
|
||||
});
|
||||
|
||||
it("still fails closed for a permission no module or Core ever declared", () => {
|
||||
const registry = composePlatformPermissionRegistry([]);
|
||||
|
||||
expect(registry.has("calendar.events.read")).toBe(false);
|
||||
expect(registry.get("calendar.events.read")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails composition on an exact duplicate permission id supplied by two module entries", () => {
|
||||
// A permission id must start with its own module's id, so a genuine cross-module collision is
|
||||
// only reachable if the supplied list itself contains the same module id twice — real
|
||||
// discovery already guarantees module-id uniqueness (`MODULE_ID_DUPLICATE`), but composition
|
||||
// re-derives duplicate detection from the flat declaration list regardless, rather than
|
||||
// trusting that upstream guarantee alone.
|
||||
const first = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
]);
|
||||
const second = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "public", description: "Also read events." },
|
||||
]);
|
||||
|
||||
expect(() => composePlatformPermissionRegistry([first, second])).toThrow(
|
||||
PermissionRegistryError,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects a module contributing a permission in the reserved core.* namespace, without leaking a module path", () => {
|
||||
// Only a module whose own id is literally "core" can produce a "core."-prefixed permission
|
||||
// that still passes the manifest's own module-namespace check — this proves composition
|
||||
// catches that case rather than trusting the manifest's self-consistency alone.
|
||||
const impostor = fakeModule("core", [
|
||||
{ id: "core.rogue.permission", baseline: "restricted", description: "Not really Core's." },
|
||||
]);
|
||||
|
||||
let captured: unknown;
|
||||
try {
|
||||
composePlatformPermissionRegistry([impostor]);
|
||||
} catch (error) {
|
||||
captured = error;
|
||||
}
|
||||
|
||||
expect(captured).toBeInstanceOf(ModulePermissionCompositionError);
|
||||
expect(captured).toMatchObject({
|
||||
code: "MODULE_PERMISSION_RESERVED_NAMESPACE",
|
||||
moduleId: "core",
|
||||
permissionId: "core.rogue.permission",
|
||||
});
|
||||
const publicText = String(captured);
|
||||
expect(publicText).not.toContain("/modules/core");
|
||||
expect(publicText).not.toContain("ekklesia.module.json");
|
||||
});
|
||||
|
||||
it("rejects a reserved-namespace collision even when it would not otherwise duplicate an existing id", () => {
|
||||
const impostor = fakeModule("core", [
|
||||
{
|
||||
id: "core.authorization.grants.manage",
|
||||
baseline: "restricted",
|
||||
description: "Attempting to shadow Core's own permission.",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(() => composePlatformPermissionRegistry([impostor])).toThrow(
|
||||
ModulePermissionCompositionError,
|
||||
);
|
||||
});
|
||||
|
||||
it("removes a module's permissions from the resulting registry once that module is absent from the supplied list", () => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
]);
|
||||
|
||||
const withCalendar = composePlatformPermissionRegistry([calendar]);
|
||||
expect(withCalendar.has("calendar.events.read")).toBe(true);
|
||||
|
||||
const withoutCalendar = composePlatformPermissionRegistry([]);
|
||||
expect(withoutCalendar.has("calendar.events.read")).toBe(false);
|
||||
});
|
||||
|
||||
it("performs no I/O: composition is synchronous and never imports a module's entrypoint", () => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
]);
|
||||
|
||||
// A synchronous return (not a Promise) from a fixture whose entrypoint deliberately resolves
|
||||
// to nothing on disk is direct evidence no filesystem or module-code operation occurred.
|
||||
const result = composePlatformPermissionRegistry([calendar]);
|
||||
expect(result).not.toBeInstanceOf(Promise);
|
||||
expect(result.has("calendar.events.read")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not mutate the supplied module list or Core's own declaration list", () => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
]);
|
||||
const modules = Object.freeze([calendar]);
|
||||
const coreDeclarationsBefore = [...CORE_PERMISSION_DECLARATIONS];
|
||||
|
||||
composePlatformPermissionRegistry(modules);
|
||||
|
||||
expect(modules).toEqual([calendar]);
|
||||
expect(CORE_PERMISSION_DECLARATIONS).toEqual(coreDeclarationsBefore);
|
||||
});
|
||||
|
||||
it("returns a registry and declarations that remain immutable, including module-contributed ones", () => {
|
||||
const calendar = fakeModule("calendar", [
|
||||
{ id: "calendar.events.read", baseline: "restricted", description: "Read events." },
|
||||
]);
|
||||
|
||||
const registry = composePlatformPermissionRegistry([calendar]);
|
||||
|
||||
expect(Object.isFrozen(registry)).toBe(true);
|
||||
expect(Object.isFrozen(registry.declarations)).toBe(true);
|
||||
for (const declaration of registry.declarations) {
|
||||
expect(Object.isFrozen(declaration)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ function manifestFor(
|
||||
} = {},
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
manifestVersion: 2,
|
||||
manifestVersion: 3,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
|
||||
@@ -41,7 +41,7 @@ function manifestFor(
|
||||
options: { dependencies?: Record<string, unknown>; schema?: string | false } = {},
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
manifestVersion: 2,
|
||||
manifestVersion: 3,
|
||||
id,
|
||||
name: `${id} module`,
|
||||
version: "0.1.0",
|
||||
|
||||
Reference in New Issue
Block a user