Skip to content

v0.68.2 β€” SDK Hardening

Choose a tag to compare

@github-actions github-actions released this 04 Apr 19:27

πŸ¦‹ Nika 0.68.2 β€” SDK Hardening

Inference as Code Β· April 4, 2026 Β· 24 commits

πŸ§ͺ Tests πŸ”§ Builtins πŸ“¦ Transforms 🌐 Providers
~9,800 62 52 7

✧ infer Β· ⎈ exec Β· β˜„ fetch Β· βŠ› invoke Β· ❋ agent


✨ This Release in 30 Seconds

The SDK gets its reckoning. 24 commits, 5 media security fixes, 8 SDK type fixes, a new builtin, and the official deprecation of nika-napi. The cancel() return type was wrong (CRITICAL), HTTP 429 and 503 status codes were inverted, and JobStatus::Pending simply didn't exist. Meanwhile, the media pipeline gains import path confinement, pipeline step limits, SVG validation, and PDF thread caps. The new nika:decode builtin bridges base64-returning providers (Gemini, fal.ai, Stability AI) into Nika's content-addressable storage. This is what Feature Freeze looks like β€” no new syntax, just relentless quality.


πŸ–ΌοΈ nika:decode β€” Base64 to CAS in One Step

Some AI providers return generated images as base64 strings instead of URLs. Gemini's image generation, fal.ai's Stable Diffusion endpoints, Stability AI's API β€” they all hand you a fat base64 blob. Before this release, getting that into Nika's media pipeline required writing it to a temp file, then importing. Awkward.

nika:decode eliminates the middleman. Feed it a base64 string, get back a CAS hash ready for the full media pipeline β€” thumbnails, format conversion, optimization, everything.

tasks:
  - id: generate
    fetch:
      url: "https://api.stability.ai/v1/generation/image"
      method: POST
      headers:
        Authorization: "Bearer {{$env.STABILITY_API_KEY}}"
      json:
        prompt: "A butterfly made of starlight"
      response: full

  - id: store
    with:
      b64: $generate.body
    invoke:
      tool: nika:decode
      params:
        data: "{{with.b64}}"
        media_type: "image/png"

  - id: thumbnail
    with:
      hash: $store.hash
    invoke:
      tool: nika:thumbnail
      params:
        input: "{{with.hash}}"
        width: 256

Tip

nika:decode auto-detects the media type from the data when media_type is omitted, but explicit is always better for API responses where the Content-Type might not be embedded in the base64 payload.

This brings the builtin tool count to 62.


πŸ” Media Pipeline Security β€” 5 Hardening Fixes

The media pipeline handles untrusted input by nature β€” user-uploaded images, downloaded files, API responses. Five targeted fixes close the remaining attack surface.

πŸ›‘οΈ SEC-5 Β· Import Path Confinement

Severity: 🟠 High

nika:import could previously import files from anywhere on the filesystem. A workflow with nika:import and a path template like {{with.user_path}} could be tricked into importing /etc/passwd or ~/.nika/secrets/vault.enc.

After: All import paths are now confined to the project's working directory. Symlinks are resolved before validation. ../ traversal is blocked. A path outside the project root returns NIKA-291 (MediaSecurityError).

πŸ›‘οΈ SEC-6 Β· Pipeline Step Limit (50)

Severity: 🟑 Medium

The nika:pipeline tool chains media operations in-memory. Without a limit, a malicious or buggy workflow could chain thousands of steps, exhausting memory. Now capped at 50 steps per pipeline invocation.

πŸ›‘οΈ SEC-7 Β· Pipeline Budget Enforcement

Severity: 🟑 Medium

Pipeline operations track estimated memory cost. The budget was calculated but never actually enforced β€” the pipeline would warn but proceed. Now the budget is checked before each step, and the pipeline aborts if the budget is exceeded.

πŸ›‘οΈ SEC-8 Β· SVG Zero-Size Guard

Severity: 🟑 Medium

nika:svg_render converts SVG to PNG via resvg. An SVG with viewBox="0 0 0 0" caused a division by zero in the rasterizer. Dimensions exceeding 10,000px in either direction could also cause excessive memory allocation.

After: Zero-size viewBoxes are rejected. Dimensions above 10,000px are rejected. Only valid, bounded SVGs proceed to rasterization.

πŸ›‘οΈ SEC-9 Β· PDF Thread Limit (4)

Severity: 🟑 Medium

nika:pdf_extract spawns threads for parallel page extraction. In a for_each loop processing many PDFs simultaneously, thread count could explode. Now capped at 4 concurrent extraction threads per process.

πŸ” Security Summary Table
ID Severity Vulnerability Fix
SEC-5 🟠 High nika:import could read outside project dir Path confined to working directory, symlinks resolved
SEC-6 🟑 Medium Unbounded pipeline step count Hard cap at 50 steps per pipeline
SEC-7 🟑 Medium Pipeline budget calculated but not enforced Budget check before each step, abort on overspend
SEC-8 🟑 Medium SVG div-by-zero + oversized allocation Reject zero-size viewBox and dims > 10,000px
SEC-9 🟑 Medium Unbounded PDF extraction threads Cap at 4 concurrent threads

πŸ”§ SDK Type Safety Overhaul β€” 8 Fixes

The TypeScript SDK (@supernovae-st/nika-client) and Python SDK both got a thorough audit. What we found was... not great. Here's every fix, explained.

πŸ”΄ CRITICAL Β· cancel() Return Type

The cancel() method was typed to return StatusResponse β€” the same type as status(). But the server returns a CancelResponse with different fields. TypeScript would compile, then crash at runtime when accessing fields that didn't exist.

Before:

const result = await client.cancel(jobId);
console.log(result.status); // runtime error: status is not a field on CancelResponse

After: cancel() correctly returns CancelResponse. Type-safe at compile time, correct at runtime.

🟠 JobStatus::Pending β€” Missing Enum Variant

When nika serve queues a job (max_concurrent reached), it sends status: "pending" over SSE. The SDK's JobStatus enum didn't have a Pending variant β€” just Running, Completed, and Failed. Receiving a "pending" event caused a parse error, breaking the entire SSE stream.

After: JobStatus.Pending is now a first-class variant. Queued jobs are reported correctly.

🟠 HTTP 429/503 β€” Inverted Status Mapping

This one's embarrassing. The SDK mapped HTTP 429 Too Many Requests to the ServiceUnavailable error class, and HTTP 503 Service Unavailable to RateLimited. The retry logic keyed off these classes β€” so rate-limited responses got no retry delay, and server overload got exponential backoff. Exactly backwards.

After: 429 β†’ RateLimited (with Retry-After header support). 503 β†’ ServiceUnavailable. A new RateLimited error variant makes the distinction explicit.

🟑 NIKA-XXX Error Code Parsing

When the server returns an error with a Nika error code (e.g., NIKA-045: Fetch timeout), the SDK was supposed to parse the NIKA-XXX code into a structured field. The regex was wrong β€” it silently dropped the code, leaving the error as an unparsed string.

After: Error codes are correctly extracted via regex and available on the error object as .nikaCode.

🟑 ArtifactInfo.format β€” Wrong Optionality

The TypeScript type declared format as string | undefined, but the server always sends the format field. Code that checked if (artifact.format) worked, but code that used artifact.format! (non-null assertion) was technically lying. More importantly, code that destructured { format = "text" } got the wrong default on every call.

After: format is string (required, non-optional). Matches the wire format.

🟑 Embedded Transport β€” 3 Fixes

The embedded transport (for running Nika as a library rather than a server) had three issues:

  • πŸ“‹ list_artifacts silently returned an empty array on error instead of propagating the failure
  • ⏸️ resume would hang indefinitely on a dead job instead of fast-failing
  • πŸ”΄ cancel sent the cancel signal but didn't abort the underlying process, leaving zombie workflows

All three are fixed. list_artifacts propagates errors. resume detects dead jobs and fails immediately. cancel properly aborts the process.

🟑 SSE Buffer β€” 1 MiB β†’ 2 MiB

Large workflow outputs (especially for_each loops with many items) could exceed the 1 MiB SSE event buffer. When this happened, the event was silently truncated β€” the SDK received partial JSON that failed to parse, breaking the stream.

After: Buffer doubled to 2 MiB. Events that would exceed the buffer are now split across multiple SSE frames.

🟑 Python SDK β€” Thread Safety + Auth Mapping

Two Python-specific fixes:

  • 🧡 Worker threads β€” The Python SDK spawns worker threads for SSE processing. Thread count was fixed at 1 regardless of workload. Now scales to CPU count with os.cpu_count().
  • πŸ”‘ Unauthorized mapping β€” HTTP 401 was mapped to a generic ApiError instead of the specific AuthenticationError. Error handling code that caught AuthenticationError never fired.
πŸ”§ SDK Fix Summary Table
Layer Fix Severity Impact
🟦 TypeScript cancel() return type β†’ CancelResponse πŸ”΄ Critical Runtime crash on cancel
🟦 TypeScript JobStatus.Pending variant added 🟠 High SSE stream break on queued jobs
🟦 TypeScript HTTP 429↔503 mapping corrected 🟠 High Inverted retry behavior
🟦 TypeScript NIKA-XXX code regex fixed 🟑 Medium Error codes silently lost
🟦 TypeScript ArtifactInfo.format β†’ required string 🟑 Medium Wrong optionality
βš™οΈ Embedded list_artifacts error propagation 🟑 Medium Silent empty array on error
βš™οΈ Embedded resume fast-fail on dead jobs 🟑 Medium Infinite hang
βš™οΈ Embedded cancel abort + SSE buffer 1β†’2 MiB 🟑 Medium Zombie process + truncated events
🐍 Python Worker thread scaling to CPU count 🟑 Medium Single-threaded bottleneck
🐍 Python 401 β†’ AuthenticationError mapping 🟑 Medium Wrong error class

⚠️ Deprecated: nika-napi

The N-API bindings package (nika-napi) is officially deprecated. Use @supernovae-st/nika-client instead β€” it's a pure TypeScript SDK that talks to nika serve over HTTP/SSE, with full type safety, automatic retry, and streaming support.

nika-napi will be removed in v0.70.

Warning

If you're importing from nika-napi in a Node.js project, migrate to @supernovae-st/nika-client now. The API surface is similar but not identical β€” see the migration guide.


πŸ› Other Fixes
  • 🧹 nika serve cancel endpoint now returns StatusResponse for SDK wire compatibility
  • πŸ“ MCP schema updated to reflect 62 tools, 50 transforms, nika:decode
  • πŸ§ͺ 15 new EmbeddedTransport tests (was 0) β€” roundtrip, error propagation, cancel abort
  • πŸ§ͺ Cross-crate ServeEvent ↔ Event wire format roundtrip test
  • πŸ›‘οΈ HTTP body truncated in error messages to prevent log flooding
  • πŸ›‘οΈ Percent-encoded path traversal (%2e%2e%2f) blocked in addition to raw ../

⬆️ Upgrade Notes

[!WARNING]
TypeScript SDK users: If you're on @supernovae-st/nika-client < 0.68.2, upgrade now. The cancel() return type change is a compile-time breaking change (in your favor β€” it was wrong before). The JobStatus.Pending variant may require updating exhaustive switch statements.

[!NOTE]
Python SDK users: The worker thread scaling change means your process may use more threads than before. If you're running in a thread-constrained environment, set NIKA_WORKER_THREADS to control the count.

[!NOTE]
Media pipeline users: If you had workflows importing files outside the project directory via nika:import, they will now fail with NIKA-291. Move the files into your project or use fetch: to download them instead.


πŸ“¦ Install
Method Command
πŸš€ Quick curl -fsSL https://raw.githubusercontent.com/supernovae-st/nika/main/install.sh | sh
🍺 Homebrew brew install supernovae-st/tap/nika
πŸ“¦ npm npx @supernovae-st/nika
πŸ¦€ Cargo cargo install nika
🐳 Docker docker run --rm ghcr.io/supernovae-st/nika:0.68.2
πŸ’» VS Code ext install supernovae.nika-lang
πŸͺŸ Scoop scoop bucket add nika https://github.com/supernovae-st/scoop-nika && scoop install nika
🐧 AUR yay -S nika-bin

πŸ” All binaries include SHA256 checksums, SLSA provenance, and macOS notarization.


Made with πŸ’œ by SuperNovae Studio β€” Paris, Open Source, AGPL-3.0

Full Changelog: v0.68.1...v0.68.2