Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
# `registry` — `ScopeRegistry`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Owns the shared, conflict-aware registry of plugin-declared scopes
|
||||
plus the `registerPluginScopes(registry, manifest)` helper the host
|
||||
calls at install time. The registry's primary job is install-time
|
||||
hygiene: two plugins claiming the same scope name (`billing:read`
|
||||
declared by both `billing` and `invoices`) must fail loudly with
|
||||
`RBAC.SCOPE_CONFLICT` instead of producing whichever-registered-last
|
||||
behaviour at request time.
|
||||
|
||||
The registry is intentionally not the source of truth for the gate
|
||||
decision. `principal.scopes` is. The registry exists to catch typos
|
||||
and cross-plugin conflicts at boot, drive the admin UI's
|
||||
list-of-available-scopes endpoint, and feed the OpenAPI emitter so
|
||||
a route declaring an undeclared scope raises a startup error.
|
||||
|
||||
## Features
|
||||
|
||||
- Conflict detection — duplicate `name` across plugins raises
|
||||
`RBAC.SCOPE_CONFLICT` with the conflicting plugin id in
|
||||
`error.context.conflictingPlugin`.
|
||||
- Idempotent re-register — same plugin, same name, same description
|
||||
is a no-op so hot reload is safe.
|
||||
- Empty-string scope name rejected with `RBAC.INVALID_SPEC`.
|
||||
- `unregisterPlugin(plugin)` removes every scope owned by that
|
||||
plugin — used at uninstall so a future install may legitimately
|
||||
claim the same name.
|
||||
- `registerPluginScopes(registry, manifest)` returns the count of
|
||||
newly-registered (non-idempotent) entries so the host can log a
|
||||
meaningful summary.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
registerPluginScopes,
|
||||
ScopeRegistry,
|
||||
} from "@suaveplan/api-rbac/registry";
|
||||
|
||||
const registry = new ScopeRegistry();
|
||||
registerPluginScopes(registry, {
|
||||
name: "billing",
|
||||
scopes: [
|
||||
{ name: "billing:read", description: "Read invoices" },
|
||||
{ name: "billing:write" },
|
||||
],
|
||||
});
|
||||
registry.has("billing:read"); // true
|
||||
registry.list(); // [ … two descriptors … ]
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Driving a per-plugin uninstall + reinstall cycle while preserving
|
||||
ordering invariants:
|
||||
|
||||
```ts
|
||||
import {
|
||||
registerPluginScopes,
|
||||
ScopeRegistry,
|
||||
} from "@suaveplan/api-rbac/registry";
|
||||
import { RBAC_CODES, RbacError } from "@suaveplan/api-rbac/error-codes";
|
||||
|
||||
const registry = new ScopeRegistry();
|
||||
registerPluginScopes(registry, {
|
||||
name: "billing",
|
||||
scopes: [{ name: "billing:read" }],
|
||||
});
|
||||
|
||||
// Cross-plugin claim → conflict.
|
||||
try {
|
||||
registerPluginScopes(registry, {
|
||||
name: "invoices",
|
||||
scopes: [{ name: "billing:read" }],
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof RbacError) {
|
||||
if (err.code === RBAC_CODES.SCOPE_CONFLICT) {
|
||||
// Drop the offending plugin, surface to operator …
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uninstall the first plugin, then the second may claim it.
|
||||
registry.unregisterPlugin("billing");
|
||||
registerPluginScopes(registry, {
|
||||
name: "invoices",
|
||||
scopes: [{ name: "billing:read" }],
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `RegisteredScope` — `{ name, plugin, description? }`.
|
||||
- `ScopeRegistry` — class with `register / has / get / list /
|
||||
unregisterPlugin` methods. Public no-arg constructor.
|
||||
- `registerPluginScopes(registry, manifest)` — iterates
|
||||
`manifest.scopes[]`, returns the count of newly-registered entries.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The byte-identical idempotence comparison covers `(plugin, name,
|
||||
description)`. A change in `description` from the same plugin is
|
||||
treated as a conflict on purpose — it would otherwise be a silent
|
||||
admin-visible mutation that bypasses the OpenSpec change discipline.
|
||||
The internal `Map` is intentionally not exposed; callers walk
|
||||
`list()` for snapshot semantics so a concurrent install cannot
|
||||
trip an iterator-invalidation bug.
|
||||
Reference in New Issue
Block a user