Appearance
Topic claims
A topic claim is an in-process assertion of authority over a subject prefix. The TopicClaimRegistry tracks which actor owns which prefix and prevents two actors from concurrently claiming the same one. It is distinct from the wire-level NATS ACL — topic claims are runtime-side discipline that catches collisions before they reach the bus.
Source:
projects/matrix-3/packages/core/src/core/security/ITopicClaim.tsandTopicClaimRegistry.ts. The doc comments cite Joe Armstrong, Mark Miller, and Butler Lampson on namespace security.
The shape
ITopicClaim (verbatim from ITopicClaim.ts:25):
typescript
export interface ITopicClaim {
prefix: string; // 'flowpad.tree' claims all flowpad.tree.*
owner: string; // 'flowpad/tree' — owning component mount
delegatedBy?: string; // parent who authorized; absent for root claims
claimedAt: number;
// ... plus rights, expiresAt
}A claim covers the prefix and everything under it. flowpad.tree claims:
| Subject | Covered? |
|---|---|
flowpad.tree | yes (exact match) |
flowpad.tree.child | yes (descendant) |
flowpad.tree.$inbox | yes |
flowpad.tree.accepts.* | yes |
flowpad | NO — claim is on flowpad.tree, parent prefix is unclaimed |
flowpad.canvas | NO — different sibling under flowpad |
Default rights
When an actor claims a prefix, it gets a default capability bundle (verbatim from TopicClaimRegistry.ts:17):
typescript
const DEFAULT_CAPABILITIES: CapabilityRight[] = ['read', 'write', 'invoke', 'emit'];Most actors don't need to specify rights manually — claiming the prefix gives them everything needed to publish, subscribe, and receive ops.
Why claiming exists
Without claim tracking, two actors could subscribe to {root}.flowpad.tree.$inbox and both receive every message. The first to handle it wins; the other's reply is dropped. Symptoms: random intermittent failures depending on which actor handles the request.
The claim registry detects this at registration time:
typescript
// runtime-side flow
const result = topicClaimRegistry.claim({
prefix: 'flowpad.tree',
owner: 'flowpad/tree',
delegatedBy: 'flowpad',
});
if (!result.granted) {
throw new Error(`Topic claim denied: ${result.reason}`);
}If flowpad/canvas already owns flowpad.tree (or a parent prefix that covers it), the claim is denied.
Prefix validation
The registry validates prefix strings before accepting a claim (TopicClaimRegistry.ts:50+):
| Rejected | Reason |
|---|---|
| empty / non-string | invalid input |
.. (path traversal) | namespace escape attempt |
> or * (NATS wildcards) | injection / over-claim |
| null bytes | injection |
| leading or trailing dots | malformed prefix |
Slashes (/) are NOT blocked, because owner mount paths use them (flowpad/tree). The prefix itself uses dots; the owner can use either.
Delegation chains
A child actor can claim a prefix under its parent's claim. The child's claim record carries delegatedBy: <parent-mount>. The registry verifies the parent's claim covers the child's prefix:
parent claim: 'flowpad' owner='flowpad/root'
child claim: 'flowpad.tree' owner='flowpad/tree' delegatedBy='flowpad/root'Without delegatedBy, only the parent's mount can claim the child prefix. With delegatedBy, the child claims authority granted by the parent.
Topic claims vs system.registry mount claims
Two distinct registries:
| Registry | Scope | Records |
|---|---|---|
TopicClaimRegistry (in-process) | this runtime's view of which actor owns which subject prefix | ITopicClaim (prefix, owner, rights) |
system.registry (cross-process) | the bus-level "who serves what" claim | IRegistryEntry (logicalMount, providerRuntimeId, runtimeWireRoot) |
The runtime-side registry catches local conflicts (two actors in the same process). The bus-level registry catches cross-process conflicts (two runtimes both claim chat.conversation). Both are necessary.
Disabled by default
MatrixRuntime exposes the registry through topicClaimRegistry but defaults enableTopicClaiming: false (projects/matrix-3/packages/core/src/runtime/MatrixRuntime.ts:135):
typescript
this._enableTopicClaiming = config.enableTopicClaiming ?? false;
this._topicClaimRegistry = config.topicClaimRegistry ?? new TopicClaimRegistry();Claiming is opt-in — packages that need strict prefix discipline turn it on. The runtime exposes the registry as an object regardless, so callers can inspect what's claimed even when enforcement is off.
Status: target state, partial. The registry is shipped; enforcement across all packages (auto-claim on actor mount, reject conflicting claims) is not yet on by default. The intent is to make it default-on once package boundaries are stable enough.
Bus-level enforcement
Topic claim discipline does not replace NATS ACL — it complements it. The wire-level boundary is still enforced by:
- NATS account permissions per authority root (subject-prefix isolation).
- Per-account import/export rules for cross-root federation.
- Browser users get a scoped JWT that pins them to their root.
Topic claims catch collisions inside a single trust domain. NATS ACL catches cross-domain attempts.
See also
- Capability tokens — fine-grained per-op rights.
- Mounts — what a prefix is.
- Mount claims (registry) — the bus-level claim.
- Singleton claims — one-owner mounts.