Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Centurylong/sanctifier/llms.txt

Use this file to discover all available pages before exploring further.

@sanctifier/sdk ships a dedicated browser build that loads the Sanctifier WASM binary via the browser’s native fetch-based WebAssembly instantiation API rather than Node’s synchronous require. The public API — types, function signatures, error classes — is identical to the Node.js entry, so code that calls analyze() is fully portable between runtimes.

Import path

// Explicit browser sub-path — always resolves to the browser build.
import { analyze, init, findingCodes, coreVersion, SanctifierError } from "@sanctifier/sdk/browser";
If your bundler is configured with a browser target condition (Vite, webpack with target: "web", Rollup with browser: true), importing from the package root "@sanctifier/sdk" resolves to the browser build automatically via the exports map in package.json.
// Also resolves to the browser build when the bundler sets the "browser" condition.
import { analyze, init } from "@sanctifier/sdk";
When writing a library that must run in both Node and browsers, use the package root import and let the bundler or runtime select the correct entry. Call await init() unconditionally — it is a no-op in Node and performs WASM loading in browsers.

How the browser entry differs from Node

Node entryBrowser entry
WASM loadingrequire() at module init time (synchronous)fetch + WebAssembly.instantiateStreaming (async)
init()No-op, returns immediatelyMust be called before analyze()
analyzeSyncExportedNot available
analyze()AvailableAvailable
findingCodes()AvailableAvailable (after init())
coreVersion()AvailableAvailable (after init())

Initialising the WASM module

The browser entry guards every API function behind an assertInitialized() check. Calling analyze() before init() throws:
Error: Sanctifier WASM is not initialized. Call `await init()` first or pass the wasm URL/bytes to init().
Call init() once, as early as possible in your application bootstrap. Subsequent calls are cheap — the module caches the initialized flag and returns immediately.
import { init, analyze } from "@sanctifier/sdk/browser";

// Call once during app startup.
await init();

// All subsequent analyze() calls are ready immediately.
const report = await analyze(sourceCode);

Passing a WASM URL explicitly

By default, init() resolves the WASM binary at the URL adjacent to the JS glue file. Most bundlers handle this automatically. If yours does not, pass the URL or binary explicitly:
// Vite: import the WASM URL as an asset reference.
import wasmUrl from "@sanctifier/sdk/wasm/web/sanctifier_bg.wasm?url";
import { init, analyze } from "@sanctifier/sdk/browser";

await init(wasmUrl);
// Manual fetch: pass a Response or BufferSource.
import { init, analyze } from "@sanctifier/sdk/browser";

const response = await fetch("/assets/sanctifier_bg.wasm");
await init(response);
The InitInput type accepted by init() matches the first parameter of the wasm-bindgen-generated initializer:
export type InitInput =
  | RequestInfo    // string URL or Request
  | URL
  | Response
  | BufferSource   // ArrayBuffer, TypedArray, etc.
  | WebAssembly.Module;

Bundler setup

Vite handles WASM files automatically when you import them with ?url. No additional plugin is required for @sanctifier/sdk.
// vite.config.ts — no special WASM configuration needed for default usage.
import { defineConfig } from "vite";

export default defineConfig({
  // The browser condition is set automatically for web builds.
});
If you import the WASM URL explicitly, Vite copies the binary to the output directory and replaces the import with the hashed asset URL at build time.
import wasmUrl from "@sanctifier/sdk/wasm/web/sanctifier_bg.wasm?url";
import { init } from "@sanctifier/sdk/browser";

await init(wasmUrl);

Complete browser example (vanilla JS)

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Sanctifier Playground</title>
  </head>
  <body>
    <textarea id="source" rows="20" cols="80" placeholder="Paste Soroban Rust source here…"></textarea>
    <br />
    <button id="analyze-btn">Analyze</button>
    <pre id="output"></pre>

    <script type="module">
      import { init, analyze, SanctifierError } from "@sanctifier/sdk/browser";

      // Initialize once when the page loads.
      await init();

      document.getElementById("analyze-btn").addEventListener("click", async () => {
        const source = document.getElementById("source").value;
        const output = document.getElementById("output");

        try {
          const report = await analyze(source, { failOn: "high" });

          const lines = [
            `${report.summary.total} finding(s) — ` +
              `critical: ${report.summary.critical}, ` +
              `high: ${report.summary.high}, ` +
              `medium: ${report.summary.medium}, ` +
              `low: ${report.summary.low}, ` +
              `info: ${report.summary.info}`,
            "",
          ];

          for (const f of report.findings) {
            const line = f.line != null ? `:${f.line}` : "";
            lines.push(`[${f.severity.toUpperCase()}] ${f.code}  ${f.location}${line}`);
            lines.push(`  ${f.message}`);
          }

          output.textContent = lines.join("\n");
        } catch (err) {
          if (err instanceof SanctifierError) {
            output.textContent =
              `Threshold breached ('${err.threshold}'): ` +
              `${err.report.summary.total} finding(s)\n\n${err.message}`;
          } else {
            output.textContent = String(err);
          }
        }
      });
    </script>
  </body>
</html>

Complete browser example (React)

import { useEffect, useState, useRef } from "react";
import { init, analyze, SanctifierError } from "@sanctifier/sdk/browser";
import type { AnalysisReport } from "@sanctifier/sdk/browser";

export function SanctifierPlayground() {
  const [source, setSource] = useState("");
  const [report, setReport] = useState<AnalysisReport | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [ready, setReady] = useState(false);

  // Initialize WASM once on mount.
  useEffect(() => {
    init().then(() => setReady(true));
  }, []);

  async function handleAnalyze() {
    setError(null);
    setReport(null);

    try {
      const result = await analyze(source, { failOn: "high" });
      setReport(result);
    } catch (err) {
      if (err instanceof SanctifierError) {
        setError(
          `${err.report.summary.total} finding(s) at or above '${err.threshold}'. ` +
          err.message
        );
        // The full report is still available via err.report.
        setReport(err.report);
      } else {
        setError(String(err));
      }
    }
  }

  return (
    <div>
      <textarea
        rows={20}
        cols={80}
        value={source}
        onChange={(e) => setSource(e.target.value)}
        placeholder="Paste Soroban Rust source here…"
      />
      <br />
      <button onClick={handleAnalyze} disabled={!ready}>
        {ready ? "Analyze" : "Loading WASM…"}
      </button>

      {error && <p style={{ color: "red" }}>{error}</p>}

      {report && (
        <ul>
          {report.findings.map((f, i) => (
            <li key={i}>
              <strong>[{f.severity.toUpperCase()}] {f.code}</strong>{" "}
              {f.location}{f.line != null ? `:${f.line}` : ""} — {f.message}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
The disabled={!ready} guard prevents the button from being clickable before init() resolves. Calling analyze() before init() throws synchronously with the message shown above, so it is important to wait for the Promise.

Handling async initialisation

init() is idempotent and caches its promise internally. Calling it multiple times is safe — only the first call performs the fetch and instantiation.
import { init, analyze } from "@sanctifier/sdk/browser";

// Safe to call from multiple places; only the first invocation does real work.
await init();
await init(); // no-op
await init(); // no-op

const report = await analyze(source);
If you need to delay rendering until the WASM is ready, gate on the init() promise in your app’s bootstrap before mounting the UI:
// main.ts — Vite / plain ESM entrypoint
import { init } from "@sanctifier/sdk/browser";
import { mountApp } from "./app.js";

await init();
mountApp(document.getElementById("root")!);

SanctifierError in the browser

The browser entry exports SanctifierError with the same shape as the Node entry. Use instanceof to distinguish it from generic errors:
import { analyze, SanctifierError } from "@sanctifier/sdk/browser";
import type { Severity } from "@sanctifier/sdk/browser";

async function runAudit(source: string, gate: Severity) {
  try {
    const report = await analyze(source, { failOn: gate });
    return { ok: true, report } as const;
  } catch (err) {
    if (err instanceof SanctifierError) {
      // err.report is the full AnalysisReport — render it even on failure.
      return { ok: false, report: err.report, threshold: err.threshold } as const;
    }
    throw err; // Re-throw unexpected errors (e.g. TypeError from bad input).
  }
}
SanctifierError carries err.report and err.threshold as readonly properties. They are always present when err instanceof SanctifierError is true — no nullability checks required.

failOn behaviour

failOn works identically in both the Node and browser entries. When set, the WASM analysis runs to completion, the full AnalysisReport is built, and then the SDK checks whether any finding’s severity meets or exceeds the threshold. If it does, the promise rejects with a SanctifierError that contains the complete report.
import { init, analyze, SanctifierError } from "@sanctifier/sdk/browser";

await init();

// Gate on "medium": rejects if any finding is medium, high, or critical.
try {
  const report = await analyze(source, { failOn: "medium" });
  console.log("No medium+ findings.");
} catch (err) {
  if (err instanceof SanctifierError) {
    console.warn(`Found ${err.report.summary.total} issue(s).`);
    // Access lower-severity findings too — they are in err.report.findings.
    const infoCount = err.report.findings.filter((f) => f.severity === "info").length;
    console.log(`(${infoCount} of those are informational only)`);
  }
}

API parity with Node.js

Every exported name, type, and interface is identical between the browser and Node entries. The only differences are:
  1. init() is a real async operation in the browser; it is a no-op in Node.
  2. analyzeSync is not exported from the browser entry.
  3. The browser init() accepts an InitInput argument (URL, Response, BufferSource, or WebAssembly.Module); the Node no-op ignores any argument.
All types — AnalysisReport, Finding, AnalyzeOptions, SeveritySummary, RawReport, CustomRule, SanctifierError, and all others — are imported from the same shared types.ts source and compiled into a single dist/types/index.d.ts declaration file referenced by both sub-path exports. See the SDK overview for full type documentation and the Node.js guide for server-side usage patterns.

Build docs developers (and LLMs) love