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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
# `topic.ts` — topic validation + tenant scoping
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Pure functions for validating event-bus topics and projecting them into
their tenant-scoped form. Every `publish` and `subscribe` going through
`eventBusService` calls `assertValidTopic` at the boundary, and the
scoped middleware handle uses `scopeTopic` / `unscopeTopic` to keep
per-tenant traffic isolated. Pulling this logic into its own module
keeps the service-layer code small and lets test code compute scoped
topics without spinning up the full plugin.
## Features
- Canonical `EVENT_TOPIC_PATTERN` (`/^[a-z0-9_\-:.]+$/`) — single
source of truth for topic syntax.
- `assertValidTopic(topic, { maxLength })` — throws `EventBusError`
(`INVALID_TOPIC`) with `context.reason ∈ "empty" | "exceeds-max-length"
| "pattern-mismatch"`.
- `scopeTopic(topic, tenantId?)` — prefixes with `tenant:<id>:` when
a tenant id is supplied.
- `unscopeTopic(scoped, tenantId?)` — inverse, returns `null` if the
scoped string does not belong to the supplied tenant.
- `topicRoot(topic)` — derives the OTel `topic_root` label.
- `DEFAULT_MAX_TOPIC_LENGTH` (256) and `TENANT_TOPIC_PREFIX`
(`tenant:`) constants for downstream reuse.
## Usage: Basic
```ts
import {
assertValidTopic,
scopeTopic,
} from "@suaveplan/api-plugin-event-bus/topic/topic";
assertValidTopic("order.created"); // OK
assertValidTopic("Order Created"); // throws EventBusError(INVALID_TOPIC)
const scoped = scopeTopic("order.created", "acme");
// "tenant:acme:order.created"
```
## Usage: Advanced
```ts
import {
DEFAULT_MAX_TOPIC_LENGTH,
scopeTopic,
topicRoot,
unscopeTopic,
} from "@suaveplan/api-plugin-event-bus/topic/topic";
function project(records: { topic: string }[], tenantId: string) {
return records
.map((r) => {
const original = unscopeTopic(r.topic, tenantId);
if (original === null) return null;
return { topic: original, root: topicRoot(original) };
})
.filter((x): x is { topic: string; root: string } => x !== null);
}
// Use the constant when tuning per-app limits — e.g. an app whose
// service mesh truncates labels at 128 may pass `maxLength: 128` to
// `assertValidTopic`.
console.log(DEFAULT_MAX_TOPIC_LENGTH);
```
## API Reference
### `EVENT_TOPIC_PATTERN: RegExp`
Frozen regex; lowercase letters, digits, `_`, `-`, `:`, `.`.
### `DEFAULT_MAX_TOPIC_LENGTH: 256`
### `TENANT_TOPIC_PREFIX: "tenant:"`
### `assertValidTopic(topic: string, options?: { maxLength?: number }): void`
Throws `EventBusError(EVENT_BUS.INVALID_TOPIC)` with a `context.reason`
discriminator on failure. Returns `void` on success.
### `scopeTopic(topic: string, tenantId?: string): string`
Returns `tenant:<id>:<topic>` when `tenantId` is non-empty; otherwise
returns `topic` verbatim. Validates `tenantId` against a stricter
regex (no `:` allowed) so callers cannot smuggle a foreign tenant
prefix through the parameter — a malformed id throws
`EventBusError(EVENT_BUS.INVALID_TOPIC)` with
`context.reason === "tenant-id-pattern-mismatch"`.
### `unscopeTopic(scoped: string, tenantId?: string): string | null`
Returns the original topic if `scoped` matches the tenant prefix.
Returns `null` if the prefix does not match (used by
`listSubscriptions` to filter foreign records). When `tenantId` is
absent the function is the identity.
### `topicRoot(topic: string): string`
Returns the substring before the first `.` or `:`. Used as the
`topic_root` label on `event_bus.published_total`.
## Implementation Notes
- The pattern intentionally excludes uppercase letters and whitespace
so topics stay safe for use as URL path segments, OTel attribute
values, and message-queue routing keys without further escaping.
- Tenant ids carried through `scopeTopic` use a stricter sub-regex
(no `:` separator) — otherwise a tenant id of `"t1:other"` could
emulate `tenant:t1:other:topic` for another tenant.
- `topicRoot` truncates at the first separator rather than producing
high-cardinality labels — OTel limits the number of distinct label
values per metric, so we keep this deliberately coarse.