Skip to main content

Runtime API

This page documents the TypeScript integration surface: the generated Consumer Projection that application code imports, and the lower-level ninka-authz/runtime API beneath it.

Authorization decisions run in-process through WASM. There is no network call, OPA binary, or Ninka-hosted service on the request path.

For .NET, see .NET Runtime API.

Application API: the Consumer Projection

Application code normally imports the generated projection rather than constructing the raw runtime directly.

import { createNinka, policies } from "@/src/generated/ninka";

const authz = await createNinka({
onDecision(event) {
logger.info(event);
},
});

const allowed = authz.check(policies.invoiceAccess, input);

The projection embeds the execution bundle and manifest/build metadata. At runtime it imports ninka-authz/runtime, reconstructs the embedded artifact shape, and lets the runtime verify and instantiate it. It does not read ninka/out/ from disk.

createNinka(options?)

async function createNinka(options?: {
onDecision?: (event: DecisionLogEntry) => void;
}): Promise<ProjectedNinka>

createNinka() verifies the embedded bundle and returns a ready Ninka instance. Await instance creation once; check() is synchronous after that.

The generated projection currently exposes one application-level option:

OptionMeaning
onDecisionEnables Decision Log observation and receives one masked DecisionLogEntry per decision. Throwing from the callback does not change the verdict.

This is a deliberately smaller surface than the raw runtime's NinkaLoadOptions. The projection already decides where policies come from and adapts onDecision to the runtime Decision Log sink.

policies

policies is a generated readonly table keyed by a name derived from each policy's Tegata id — hyphens removed, the following letter capitalised into camelCase.

policies.invoiceAccess
policies.a1

The derivation is many-to-one over arbitrary strings, so the compiler refuses two policy ids that would derive the same name before they can reach a generated projection — the derived key is always collision-free in code that compiles. The policy id itself is never converted: it stays on the reference, exact and untransformed (policies.invoiceAccess.id === "invoice-access").

Each generated reference carries:

  • id — exact policy id;
  • tegataHash — specification hash from the compile that produced the projection;
  • a TypeScript-only phantom binding to that policy's generated input type.

That binding lets the generated check() surface reject a wrong policy/input pairing at TypeScript compile time.

check(policy, input)

The projection narrows Ninka.check() to generated policy references:

const allowed: boolean = authz.check(policies.invoiceAccess, input);

check() is synchronous and returns:

  • true — allow;
  • false — deny.

Before policy evaluation, the runtime verifies caller-level preconditions such as a usable policy reference, a loaded policy id, matching tegataHash, top-level input object shape, and the shared maximum input depth. Violations of those API/integrity preconditions throw; they are not converted into policy deny decisions.

Missing policy attributes and wrong attribute value types inside an otherwise valid input object remain authorization inputs. The compiled three-valued semantics decide the verdict; the runtime does not replace that decision with a policy-wide pre-validation error.

Application responsibilities

The projection does not decide:

  • where the Ninka instance is stored;
  • when authorization is called;
  • which concrete resource is being authorized;
  • how session/domain data is mapped into the input;
  • how the application responds to false.

Those are application responsibilities. Ninka does not scaffold or own the application's composition root.


Raw runtime module: ninka-authz/runtime

The raw runtime is useful for directory-based loading, tooling, tests, and understanding the implementation beneath the generated projection.

The documented application-relevant exports include:

  • Ninka
  • AuthzInput
  • PolicyRefLike
  • EmbeddedBundle
  • InputValidationError
  • DecisionLogEntry
  • DecisionLogOptions
  • NinkaLoadOptions

The package also exposes internal/conformance plumbing that is not part of the normal application API. Application integrations should prefer the generated Consumer Projection.

The runtime package's production dependency is @open-policy-agent/opa-wasm.

class Ninka

The constructor is private. Instances are created through verified acquisition paths.

APIReadsTypical use
Ninka.load(dir, options?)artifact directorytooling, scripts, applications that deploy artifacts as files
Ninka.fromEmbedded(bundles, options?)parsed metadata + base64 modules embedded in codegenerated Consumer Projection

Both paths converge on the same bundle-verification core.

Ninka.load(dir?, options?)

static async load(
dir = "ninka/out",
options: NinkaLoadOptions = {},
): Promise<Ninka>

Discovers execution bundle build records under dir/bundles/, reads the corresponding WASM and policy manifests, verifies the complete bundle, and returns a ready instance.

For each bundle the shared verification core checks requirements including:

  • module bytes against build.json.wasm_sha256;
  • build-file name and bundle_id agreement;
  • binding policy_id and manifest identity;
  • supported artifact_format_version on the build record and manifests;
  • manifest/binding rego_sha256 lineage;
  • non-empty manifest tegata_hash;
  • manifest/binding entrypoint agreement;
  • that each declared entrypoint exists in the WASM entrypoint table;
  • that the module exposes an inspectable builtins table and requires no host builtins;
  • uniqueness of loaded policy ids.

A required integrity failure refuses the whole bundle. The runtime does not continue by serving only the bindings that happened to verify.

load() does not read source Tegata and therefore cannot tell whether source has changed since the artifacts were compiled. Source/artifact drift is the job of tooling that can see both sides, such as ninka-authz verify and LIVE Authorization Reference.

Ninka.fromEmbedded(bundles, options?)

static async fromEmbedded(
bundles: EmbeddedBundle[],
options: NinkaLoadOptions = {},
): Promise<Ninka>

Low-level loader for bundles already carried in a module graph. An EmbeddedBundle contains:

  • build-record file name;
  • parsed build record;
  • WASM as base64;
  • parsed manifests keyed by policy id.

The runtime decodes the module and passes the reconstructed artifacts through the same verification core used for directory loading.

Application code normally reaches this path indirectly through generated createNinka().

Raw Ninka.check()

The public runtime signature accepts PolicyRefLike:

check(policy: PolicyRefLike, input: AuthzInput): boolean

The runtime also has checkByPolicyId, a separate method for internal verification and CLI plumbing. It is not an overload of check(): check() throws when it is handed a policy id string, in JavaScript as well as in TypeScript. checkByPolicyId is marked internal and is stripped from the published types.

For a generated reference, check() compares policy.tegataHash with the loaded manifest's tegata_hash. A missing or mismatched hash throws rather than evaluating a policy under the wrong generated contract.

The top-level input must be a non-null, non-array object. Unknown policies, malformed references, a projection/bundle hash mismatch, and input deeper than the shared 64-container-level limit are caller/integrity errors and throw.

Within a valid top-level input object, missing attributes and runtime type mismatches are left to the compiled authorization semantics and normally result in deny according to those semantics rather than becoming API-shape exceptions.

policyIds

get policyIds(): string[]

Returns the ids currently loaded by the runtime. This is runtime introspection, not the generated type-safe application selection API. Application code should normally select from policies.<name>.


AuthzInput

export interface AuthzInput {
subject: { properties: Record<string, unknown> };
action: { name: string };
resource: { type: string; properties?: Record<string, unknown> };
context?: Record<string, unknown>;
}

Tegata keys map to runtime input as follows:

Tegata keyRuntime input
actionaction.name
resource_typeresource.type
subject.*subject.properties.*
resource.*resource.properties.*
environment.*context.*
custom.*context.custom.*

The Consumer Projection generates a narrower input type for each policy and binds it to the corresponding policies reference.

Input nesting is limited to 64 container levels. A deeper input throws as a caller-contract error rather than becoming an authorization deny.


NinkaLoadOptions

export interface NinkaLoadOptions {
onInputError?: (error: InputValidationError) => void;
decisionLog?: DecisionLogOptions;
}

These are raw runtime options. The generated projection exposes the narrower onDecision option described earlier.

onInputError

export interface InputValidationError {
policyId: string;
missing: string[];
}

When a decision is deny, the runtime can report required input keys that are absent according to the manifest requirements associated with the policy's rule origins.

This callback is observational only. It does not alter or replace the WASM verdict. A throwing callback is contained so observation cannot change the decision.

If no callback is supplied, the current TypeScript runtime writes a compact diagnostic to stderr. The exact number of lines is not a runtime contract.

decisionLog

export interface DecisionLogOptions {
enable?: boolean;
sink?: (entry: DecisionLogEntry) => void;
}

Decision logging is off by default. With enable: true, each check() emits one DecisionLogEntry to the supplied sink, or to the default JSON-lines stdout sink when no sink is supplied.

A sink that throws does not alter the decision; the runtime warns and continues.

The logged input is not a redacted clone of every field the caller supplied. The runtime first projects the input onto the declared AuthzInput shape, then masks the retained values:

  • action.name and resource.type are always recorded raw;
  • subject/resource/custom/environment attributes appear raw only when permitted by decision_log.unmasked;
  • other retained values are masked as "***";
  • top-level keys outside the declared AuthzInput shape are omitted from the log representation even though the original input object is what the WASM evaluated.

See Vocabulary for the disclosure configuration.


DecisionLogEntry

Ninka follows the OPA Decision Log field model only where Ninka's artifact contract defines the semantics as equivalent. Similar field names do not imply complete OPA compatibility.

FieldMeaning
decision_idunique decision id
timestampdecision timestamp
path"ninka/result"
resultboolean verdict
inputprojected and masked log representation described above
bundlespolicy id → { revision }, where revision is that manifest's tegata_hash
labelsruntime/operational labels, including Ninka's policy_id label
ninkaNinka extension: { schema: 1, policies: [{ id, verdict, missing? }] }

The field set is extensible. Consumers should read fields they understand and tolerate additional compatible fields instead of validating entries against a permanently closed object shape.

See also