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:
| Option | Meaning |
|---|---|
onDecision | Enables 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:
NinkaAuthzInputPolicyRefLikeEmbeddedBundleInputValidationErrorDecisionLogEntryDecisionLogOptionsNinkaLoadOptions
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.
| API | Reads | Typical use |
|---|---|---|
Ninka.load(dir, options?) | artifact directory | tooling, scripts, applications that deploy artifacts as files |
Ninka.fromEmbedded(bundles, options?) | parsed metadata + base64 modules embedded in code | generated 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_idagreement; - binding
policy_idand manifest identity; - supported
artifact_format_versionon the build record and manifests; - manifest/binding
rego_sha256lineage; - 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 key | Runtime input |
|---|---|
action | action.name |
resource_type | resource.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.nameandresource.typeare 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
AuthzInputshape 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.
| Field | Meaning |
|---|---|
decision_id | unique decision id |
timestamp | decision timestamp |
path | "ninka/result" |
result | boolean verdict |
input | projected and masked log representation described above |
bundles | policy id → { revision }, where revision is that manifest's tegata_hash |
labels | runtime/operational labels, including Ninka's policy_id label |
ninka | Ninka 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.