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:
ekklesia
2026-07-24 12:25:00 -05:00
parent 79aaab93b6
commit d9dd409aa3
20 changed files with 765 additions and 52 deletions
+12 -6
View File
@@ -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>;