Skip to content

Portals

A charter is where an application constitutes its authority. Each declaration hands back the one object able to perform that operation — a portal.

portal what holding it lets you do
Occlude<T> hand over a real value, leave with an occluded reference
Reveal<T> turn an occluded reference back into a value, at one declared sink
Derivation<I,O> make one value from another
Fold<I,O> make one value from many
Query<I,Q> ask one question of a value without the value leaving
Erasure forget a value and everything derived from it — see Erasing
Inspection read a value's label and lineage, without the value

Authority is held, never looked up

There is deliberately no method that trades a name for the portal it names. A name is what the manifest and the audit trail call a portal; it is not a way through it.

Miller's four ways to come by a capability are initial conditions, parenthood, endowment and introduction. Lookup by name is not among them, and adding it would quietly return this library to policing labels rather than distributing authority.

The practical consequence: "which code in this application can read cardholder data?" is answered by grepping for a constructor parameter.

public final class ChargeCard {
  private final Reveal<String> toGateway;      // this class can. Nothing else can.

  public ChargeCard(Reveal<String> toGateway) {
    this.toGateway = toGateway;
  }
}

Occlude takes no label

Occlude<Mail> customerMail =
    charter.source("customer-mail", MAIL, ctx -> label(ctx, UNENDORSED, PERSONAL));

Occluded<Mail> held = customerMail.occlude(incoming);   // no label argument

The portal carries its own label, decided once when it was declared, so code holding it writes at that label and no other. A service handed the portal for customer-submitted disputes cannot create cardholder data — not "is refused at runtime", but cannot express the operation, because the only portal it has says something else. Writing at another tenant's label is less refused than unsayable.

The label may still depend on who is acting: a portal fixes what is a property of the portal itself — what arrives there, how far it is trusted, how sensitive it is — and reads the tenant from ambient context. So it is not quite a constant, but nothing a caller passes influences it. A source takes its label in one of three shapes:

charter.source("notes", NOTE, Label.of(TENANT, "acme"));                  // always the same
charter.source("customer-mail", MAIL, ctx -> label(ctx, UNENDORSED, PERSONAL)); // who is acting
charter.source("claims", CLAIM, (claim, ctx) -> labelFor(claim, ctx));    // and what arrived

The last reads the arriving value itself — a claim that mentions a card is labelled CARDHOLDER, say. It is the portal's function doing it, declared once and listed in the manifest, not the caller. Occluding is refused with a RefusedException when the function cannot say (SOURCE_CANNOT_LABEL) or leaves a required axis unsaid (INCOMPLETE_LABEL).

Occluding is the axiom

Occluding is where data enters, and at that moment there is no earlier label to check against. Monotone join makes it a theorem that derivation cannot weaken a label; occluding is the axiom that theorem rests on. Declaring portals makes those axioms enumerable, which is all anything can do.

There is no reveal without a sink

Reveal<Invoice> paymentProcessor =
    charter.reveal("payment-processor", ctx -> ceiling(ctx, ENDORSED, CARDHOLDER), INVOICE);

// a portal that reads several types is declared once, and read through per type:
Sink vendor = charter.sink("vendor", ctx -> ceiling(ctx, ENDORSED, ORDINARY), MAIL, NOTE);
Reveal<Mail> vendorMail = vendor.reading(MAIL);

Revealed<Invoice> out = paymentProcessor.reveal(held);

No overload omits the sink. You cannot obtain plaintext "in general", only plaintext for somewhere — and that somewhere is what the ceiling hangs off and what the audit records.

Revealed<T> is a sealed result rather than an exception: being turned away while reading is an ordinary outcome worth branching on.

switch (out) {
  case Revealed.Allowed<Invoice> allowed -> send(allowed.plaintext());
  case Revealed.Denied<Invoice> denied   -> log(denied.reason());
}

Asking a question

A question looks at the value and returns one bit, and the value never leaves:

Query<Mail, String> mentions = charter.query("mail.mentions", MAIL, String.class,
    (mail, text, ctx) -> mail.body().contains(text),
    q -> q.accepting(ctx -> ceiling(ctx, UNENDORSED, PERSONAL))
          .availableTo(ctx -> ctx.has("handles-disputes", "true")));

Answer answer = mentions.ask(held, "INV-4471");
if (answer.isTrue()) { ... }         // ran, and said yes
if (answer.isFalse()) { ... }        // ran, and said no -- a refusal is neither

accepting says which values it may look at; availableTo, optional, says in which contexts it is offered at all. Ask it against plain values only: a question handed a Consumer or a list could put the value there, and the manifest lists any that are not, as a not-a-plain-value finding. Enough questions read a value a bit at a time — see What Occlude Does Not Do.

Reading a label

An Inspection reads what the record says about a value — its label and its lineage — without the value. It is a portal of its own because a label can say as much as the value it describes:

Inspection supportDesk = charter.inspection("support-desk",
    ctx -> Ceiling.of(TENANT, Constraint.atMost(ctx.get("tenant").orElseThrow())));

supportDesk.inspect(held).value()
    .ifPresent(seen -> render(seen.label(), seen.lineage()));

What comes back

Every portal but Occlude returns a sealed result rather than throwing, because a refusal is an ordinary outcome: a prompt renders the handle instead, a tool reports that it cannot proceed. Each has value() — what it produced, when it produced anything — succeeded(), and orThrow() for code that cannot go on without it, which throws a RefusedException naming the reason.

portal result when it succeeds reasons it refuses
Reveal Revealed Allowed(plaintext) NO_SUCH_VALUE, WRONG_TYPE, ABOVE_CEILING
Derivation, Fold Derived Made(occluded) NO_PARENTS, NO_SUCH_VALUE, WRONG_TYPE, ABOVE_CEILING, NOT_AVAILABLE_HERE, NOT_A_LOWERING, DECLINED, FAILED
Query Answer Answered(value) — see isTrue() / isFalse() NO_SUCH_VALUE, WRONG_TYPE, ABOVE_CEILING, NOT_AVAILABLE_HERE, FAILED
Erasure Erased Removed(count) NO_SUCH_VALUE, NOT_PERMITTED
Inspection Inspected Seen(label, lineage) NO_SUCH_VALUE, ABOVE_CEILING

The reason is a code that names a rule and never a value; each refusal also carries a detail for whoever handles it. When application code a portal runs — a ceiling, a derivation — throws, the refusal says which exception, by class name, and never its message. When that code is a derivation's or a query's own function, already handed the plaintext, the reason is FAILED: a fault, so a caller can tell it from a DECLINED or a NOT_AVAILABLE_HERE without reading the record.

Occluded references disclose nothing

An Occluded<T> prints as its identifier and nothing else, and carries no runtime type. Knowing that an occluded reference is a card token rather than a display name is itself a disclosure, so a refusal will not tell you either — the ceiling is checked before the type, and a caller who may not see the value is not told what kind of value it is. (Its identifier does disclose when the value was made — see What Occlude Does Not Do.)

The lifecycle

A charter is declared, then bound to storage and to where identity comes from, once and irreversibly. Before binding, no portal will work; after it, no further authority can be constituted — and the charter has no further part to play, because every portal carries everything its operation needs.

Whoever constructs a charter keeps the ability to bind it, and that reference is not on the Charter interface every bean is handed. Erasing a value and reading its label are portals like the rest — Erasure and Inspection — so the interface declares portals and reports on the declarations, axes() and manifest(), which describe the system and never a value.