KloradDocs

Worlds and access

One twin, many entitled views. A World decides which objects a group sees, what it may do, and who may enter.

A site has an operations room, maintenance crews, contractors and sometimes the public. They look at the same twin, but each should see only what is theirs and do only what they are entitled to. Klorad does not make a copy of the twin per audience; it gives each audience a World.

What a World is

A World is a named, entitled view over one twin:

FieldDecides
scopeWhich objects it contains: all of them, a list of ids, objects of a given correspondence, objects whose properties match, or objects within a radius of a point.
capabilities.actionsWhich Actions may be taken in it: "*" or a list of ids.
principalsWho may enter: users, teams or roles. An empty list means nobody.
presentationHow it looks: colours, an initial camera, whether it is for signed in users or public.

Entitlement is an intersection

An actor may take an Action on an object when all of these hold:

  1. one of the actor's organisation roles allows the Action;
  2. the request names a World and the actor may enter it;
  3. the object is inside the World's scope;
  4. the World's capabilities allow the Action.

Any one failing denies the request, and the reason says which. createAccessPolicy returns an AccessPolicy that is the Entitlement the World layer asks.

Members receive a projection

policy.view(worldId, actor) returns a projection: a scene that contains only the in scope objects and their readings, kept in step with the twin as it changes. Give that scene to the member's interface and nothing outside the World ever reaches their browser. An object that moves out of scope, or whose properties stop matching, disappears from the view.

worlds.ts
import { auditActions, createAccessPolicy, createMemoryAuditLog, type World } from "@klorad/access";
import { createScene } from "@klorad/api/world";

const scene = createScene({ coordinateSystem: { origin: { lat: 40.62637, lon: 22.94838 } } });
scene.add({ id: "n1", name: "Bin N1", position: { east: -30, north: 70 }, correspondence: "shadow", binding: { source: "bins", entity: "n1" }, properties: { district: "north" } });
scene.add({ id: "s1", name: "Bin S1", position: { east: -25, north: -80 }, correspondence: "shadow", binding: { source: "bins", entity: "s1" }, properties: { district: "south" } });

const worlds: World[] = [
  { id: "city", name: "City operations", scope: { all: true }, capabilities: { actions: "*" }, principals: [{ kind: "role", id: "admin" }] },
  {
    id: "north",
    name: "North crew",
    scope: { properties: { district: "north" } },
    capabilities: { actions: ["mark-emptied"] },
    principals: [{ kind: "team", id: "crew-north" }],
    presentation: { primary: "#0e7490", visibility: "authenticated" },
  },
];

const audit = createMemoryAuditLog();
const policy = createAccessPolicy({ scene, roles: { admin: "*", crew: ["mark-emptied"] }, worlds, audit });
auditActions(scene, audit);

const kostas = { id: "kostas", roles: ["crew"], teams: ["crew-north"] };
const entered = policy.worldsFor(kostas).map((w) => w.name); // ["North crew"]

// What Kostas receives: a scene with Bin N1 only, kept in step with the twin.
const view = policy.view("north", kostas);
const visible = view.scene.objects().map((o) => o.id); // ["n1"]

export { entered, visible };

The audit log

auditActions(scene, sink) writes every Action outcome, allowed or denied, to an append only sink, and the policy writes every World change made through setWorld and removeWorld. createMemoryAuditLog is the in memory sink for demos and tests; a server app implements AuditSink over a database table.

Run it liveSandbox: WorldsOne twin, entitled views: the city administrator sees every bin, a district crew sees and acts on its own.

Something wrong or unclear? Tell your contact at Prieston Technologies.