Joshua Damon
FlagshipActive2026

SentinelRN

Runtime Integrity & AI Security SDK · React Native

An open-source security SDK that answers the question most mobile security libraries leave unanswered: "can I trust this environment enough to perform this action?" It detects compromised runtimes (root, jailbreak, emulators, hooking, tampering) and inspects AI-bound input for secrets, PII, and prompt injection — returning structured, explainable threat reports instead of bare booleans. Built end-to-end as creator and sole engineer.

Tech Stack

TypeScriptReact NativeKotlinSwiftTurborepopnpmVitestBiome

What I Owned

  • ·Trust core: risk scoring & policy engine
  • ·AI subsystem: secret/PII/injection detection
  • ·Native detectors (Kotlin + Swift)
  • ·React bindings, monorepo & release tooling

Engineering Focus

  • ·One noisy-OR risk engine for every signal
  • ·Fail-open — a detector crash is never an app crash
  • ·Strict module boundaries, injected detectors
  • ·Explainable, structured threat reports

Security

  • ·No network, no secret storage, no egress
  • ·Secure-by-default (monitor mode, redaction on)
  • ·On-device redaction before LLM calls
  • ·Honest about uncertainty — probabilistic by design

Performance

88unit tests
30+detection rules
3published pkgs
Engineering Case Study
Internal Document

An open-source (MIT) security SDK for React Native that answers a question most mobile security libraries leave unanswered: "can I trust this environment enough to perform this action?" It detects compromised runtimes (root, jailbreak, emulators, debuggers, hooking, tampering) and inspects AI-bound input for secrets, PII, and prompt injection — then returns structured, explainable threat reports instead of bare booleans.

Stack at a glance: TypeScript (strict, verbatimModuleSyntax) · React Native (Expo + bare) · Kotlin · Swift · pnpm + Turborepo monorepo · tsup (ESM + CJS + .d.ts) · Vitest (88 tests / 12 suites) · Biome · Changesets · Expo example app (New Architecture)

Links: GitHub · Live site · published as @sentinelrn/core, @sentinelrn/native, @sentinelrn/react.


1. Executive summary

React Native increasingly powers apps that handle payments, health data, identity, and AI prompts — running on devices that may be rooted, jailbroken, emulated, hooked, or repackaged. The existing ecosystem solves slivers of this: one library detects root, another does secure storage, a third filters PII. Each returns a boolean. None answer the question an application actually has: given everything I can observe, is it safe to do this right now?

SentinelRN is built around four hard problems, and the architecture is essentially the set of answers to them:

Hard problemArchitectural answer
Booleans don't support real decisionsA risk engine that turns weighted signals into a 0–100 score, a coarse level, and a recommended action
Device + AI risk are scored inconsistentlyOne scoring model (ScorableassessRisk) shared by integrity reports and AI findings
Security logic leaks into UI and platform codeStrict dependency rulescore is pure TS; native and React only feed/consume it
A failing detector can crash the host appFail-open orchestration — provider errors degrade to a clean, empty report

The codebase is a pnpm + Turborepo monorepo with three published packages (@sentinelrn/core, @sentinelrn/native, @sentinelrn/react) plus an Expo example app. The data flow is a single, testable pipeline: provider → integrity → threat → risk → report → policy → app.


2. My role

Creator and sole engineer, end to end:

  • The trust core — risk scoring, threat normalization, the policy engine, and the cohesive SentinelRN facade, all in pure TypeScript.
  • The AI subsystem — secret/PII pattern detectors, prompt-injection heuristics, overlap resolution, and reversible redaction.
  • Native detectors — Android (Kotlin) root/emulator/hooking checks and iOS (Swift) jailbreak/debugger checks, bridged through one provider contract.
  • Developer experience — the React layer, secure-by-default configuration, the published monorepo, release tooling, and the example app.

3. Technology stack & why

ConcernChoiceWhy this over alternatives
Core languageTypeScript (strict)The engine must be platform-agnostic and tree-shakeable; pure TS runs in Expo, bare RN, and tests with no native dependency
Module hygieneverbatimModuleSyntax + ESM-firstForces explicit import type, keeps the boundary between types and runtime code unambiguous
BundlingtsupEmits ESM + CJS + .d.ts from one config — broad consumer compatibility without hand-rolled build steps
Monorepopnpm workspacesStrict, content-addressed installs; clean inter-package linking for the three packages
Task graphTurborepoCached build/test/typecheck across packages; only what changed re-runs
TestsVitestFast, ESM-native, jsdom for the React hooks; 88 cases keep the scoring math honest
Lint + formatBiomeOne fast tool replacing ESLint + Prettier; less config surface to maintain
Android detectorsKotlin (TurboModule)First-class access to Build, Debug, Settings, the package manager, and the filesystem
iOS detectorsSwiftSandbox probes, symlink checks, sysctl/ptrace-style debugger detection, dyld inspection
React bindingsReact + contextIdiomatic provider/hook surface; the layer holds no security logic, only state plumbing
DemoExpo (New Architecture)Proves the SDK works end-to-end on a real device with the New Architecture enabled
ReleasesChangesetsVersioned, reviewable releases across the workspace

4. System architecture (the big picture)

Layered structure. Everything flows through the core engine; nothing reaches around it. SentinelRN sits between the React Native app and the actions that matter, continuously evaluating the runtime and AI-bound input to produce explainable trust decisions.

High-level architecture — SentinelRN sits between the React Native app and sensitive actions, fanning out to device integrity, AI input security, and the policy engine, which collapse into one explainable threat report that drives the application decision (allow / warn / monitor / block).
High-level architecture — SentinelRN sits between the React Native app and sensitive actions, fanning out to device integrity, AI input security, and the policy engine, which collapse into one explainable threat report that drives the application decision (allow / warn / monitor / block).

Runtime data flow (one pipeline). A platform provider emits loosely-typed RawSignals → the threat engine normalizes them into complete ThreatSignals (stable id, default severity/confidence, human message) → the risk engine scores the set → a ThreatReport is assembled → the policy engine turns the report into an allow/block PolicyDecision with reasons → the app enforces. The AI path reuses the same risk math: AIFindings are Scorable, so device risk and prompt risk are measured on one ruler.

Dependency rules (enforced by design).

core    ──>  (nothing)
native  ──>  core          (implements IntegrityProvider)
react   ──>  core          (consumes the SentinelRN API)
example ──>  core + native + react

Nothing depends on React except @sentinelrn/react. core contains no platform code — detectors are injected at runtime via the IntegrityProvider interface, which is what lets Play Integrity, App Attest, or SSL-pinning providers slot in later without changing the public API.


5. Package entry points & bootstrap

The public surface is deliberately tiny — a single import, a handful of namespaces:

import { SentinelRN } from "@sentinelrn/core";

SentinelRN.configure(...)   // optional — secure defaults otherwise
SentinelRN.integrity        // runtime-integrity checks
SentinelRN.ai               // PromptGuard
SentinelRN.policy           // reports → decisions
SentinelRN.redaction        // redact / inspectAndRedact
SentinelRN.version

Singleton + factory. createSentinel(config) builds an isolated instance (invaluable for deterministic tests); the app-wide SentinelRN singleton is just createSentinel(). The integrity module reads config through a getConfig() thunk, so a later configure() takes effect live without re-wiring.

Bootstrap order. Native registration happens once at startup, before the first check:

registerSentinelNative();                 // wire native detectors into the singleton
SentinelRN.configure({ policy: "warn" }); // optional

registerSentinelNative() simply calls registerIntegrityProvider() on the target instance — the React SentinelProvider defaults to that same singleton, so registration done at the entry point is visible everywhere downstream.


6. Feature domains (the modules)

@sentinelrn/core is a set of small, independently-testable modules with a strict one-directional dependency graph:

ModuleResponsibilityNotably does not
types/Shared vocabulary: severity, confidence, reports, findings, config, errors
risk/severity + confidence → score, level, recommended actionKnow what a "signal" means
threat/raw signals → normalized signals → ThreatReportDecide app behavior
ai/secret/PII detection, injection heuristics, redaction, PromptGuardTalk to any LLM provider
policy/reports → allow/block decisions with reasonsCollect runtime information
integrity/orchestration + the IntegrityProvider contractContain platform code
core/config resolution + the assembled SentinelRN facadeContain platform logic

Each module owns one job. The risk engine, for instance, scores anything with a severity and confidence — it has no idea whether it's grading a jailbreak signal or a leaked API key, which is exactly why the two stay consistent.


7. Data flow & the signal pipeline

This is the heart of the SDK — where heuristic observations become a defensible number.

The runtime trust pipeline — raw signals are normalized into complete threat signals, scored by the noisy-OR risk engine, assembled into a threat report, and arbitrated by the policy engine into an application decision, with every step explainable.
The runtime trust pipeline — raw signals are normalized into complete threat signals, scored by the noisy-OR risk engine, assembled into a threat report, and arbitrated by the policy engine into an application decision, with every step explainable.

Normalization. Detectors are allowed to be lazy: a RawSignal can be as little as { type: "jailbreak", platform: "ios" }. The threat engine fills in the rest from a per-type defaults table. Those defaults encode the threat model directly:

Signal typeDefault severityDefault confidenceRationale
hooking, tamperingcriticalmediumActive runtime manipulation — the most dangerous, but inherently noisy
root, jailbreakhighmediumStrong weakening of platform protections; detection is bypassable
emulator, debugger, mock_locationmediumhighReliable to detect, moderately concerning
simulator, developer_modelowhighAlmost certainly a developer, not an attacker

Scoring (noisy-OR). Rather than summing weights (which overflows) or taking a max (which ignores corroboration), the risk engine treats each signal as an independent probability that something is genuinely wrong and combines them as a probabilistic OR:

score = (1 − Π(1 − pᵢ)) × 100,  where pᵢ = SEVERITY_WEIGHT[s] × CONFIDENCE_MULTIPLIER[c] / 100

SEVERITY_WEIGHT       = { low: 12, medium: 35, high: 65, critical: 92 }
CONFIDENCE_MULTIPLIER = { low: 0.5, medium: 0.8, high: 1 }
RISK_THRESHOLDS       = { low: 0, medium: 20, high: 50, critical: 85 }

The noisy-OR risk engine — each normalized signal becomes a probability from its severity weight and confidence multiplier; combining them rewards corroboration and saturates toward 100 instead of overflowing, then maps to a risk level and a recommended action.
The noisy-OR risk engine — each normalized signal becomes a probability from its severity weight and confidence multiplier; combining them rewards corroboration and saturates toward 100 instead of overflowing, then maps to a risk level and a recommended action.

More signals push the score up but it saturates gracefully toward 100 instead of overflowing. A single high-severity/high-confidence signal lands in high; a critical signal (or a cluster of high ones) reaches critical. A device is "compromised" once risk hits high, and the recommended action ladders from allowmonitorwarn_userblock_sensitive_actionblock_session. hasSignals distinguishes a genuinely clean device (allow) from a quiet-but-noisy one (monitor).

Fail-open orchestration. If no provider is registered, or a provider throws, integrity.check() swallows it and returns a clean, empty report. A broken detector is a detection gap — never a crash.


8. The AI subsystem (PromptGuard)

PromptGuard inspects AI-bound text on-device and returns an explainable, policy-aware decision. It never sends anything anywhere — SentinelRN protects prompts; it is not an AI SDK.

The AI PromptGuard pipeline — AI-bound input is scanned for secrets, PII, and prompt injection; matched secrets and PII are redacted into a safe prompt before anything reaches the LLM, while policy decides whether injection attempts are warned on or blocked.
The AI PromptGuard pipeline — AI-bound input is scanned for secrets, PII, and prompt injection; matched secrets and PII are redacted into a safe prompt before anything reaches the LLM, while policy decides whether injection attempts are warned on or blocked.

Detection layers.

  • 15 secret detectors — OpenAI/Anthropic/Google/Stripe/SendGrid API keys, AWS access keys, GitHub tokens & fine-grained PATs, npm tokens, Slack tokens & webhooks, Twilio SIDs, PEM private-key blocks, JWTs, plus looser password:/Bearer assignment patterns (captured at group level so the label survives and only the value is redacted).
  • 5 PII detectors — email, phone, IPv4, SSN (rejecting reserved ranges), and credit cards (validated with a Luhn checksum to cut false positives).
  • 10 injection rules — ignore-previous-instructions, reveal-system-prompt, override-guardrails, DAN-style jailbreaks, unrestricted-persona, fake [system]/<im_start> markers, verbatim-repeat extraction, and encoded-payload obfuscation.

Overlap resolution. Detectors run in priority order — secrets beat PII beat injection markers. Overlapping spans are resolved by start index, then priority, then length, and survivors are re-sorted into reading order. So a JWT that also matches a generic email-ish pattern is reported once, as the JWT.

Reversible, structured redaction. Sensitive values are replaced from the end of the string backward (so earlier indices stay valid) with stable tokens like [REDACTED_CREDIT_CARD]. Findings carry a masked preview (abc•••••), never the raw value, unless the app explicitly opts in. Injection findings are reported but not redacted — the surrounding text is the payload, so policy (not redaction) decides whether to block it.

Policy-aware decision. Each finding argues for an action (secrets/PII → redact; high-severity injection → block; otherwise warn); the strongest wins. The four policies then arbitrate: monitor/warn never block, block stops high-severity or block-worthy content, and strict blocks on any finding at all.


9. Native integrity detectors

The native package is the only place platform code lives. Both platforms emit a flat boolean integrity snapshot; the JS provider maps each set flag to a RawSignal and lets the core threat engine assign severity/confidence — so scoring stays identical across platforms.

Android (Kotlin). Root detection is layered — su binary paths, which su, known root & cloaking packages, busybox, test-keys build tags, writable system paths, and ro.debuggable/ro.secure build props. Plus emulator fingerprinting (goldfish/ranchu/Genymotion/generic builds), debugger attachment, developer mode, mock location, hooking, and tampering.

iOS (Swift). Jailbreak detection combines known filesystem paths, a write-outside-the-sandbox probe, suspicious symlinks on /Applications, and canOpenURL checks for cydia:///sileo:///zbra:// schemes — short-circuiting to false on the simulator. Plus simulator, debugger, and hooking checks.

Graceful fallback. If the native module is absent (e.g. Expo Go, web), the provider degrades to weak JS-only heuristics rather than throwing — and the docs are explicit that real integrity detection requires the native module.


10. State management & configuration

There is no global mutable state beyond a single registered provider per instance. Configuration is resolved by merging a partial config over a base, producing a fully-populated ResolvedConfig — and the defaults are deliberately the safe choice:

DEFAULT_CONFIG = {
  policy: "monitor",                                    // observe before you block
  integrity: { includeEvidence: false },               // don't leak raw evidence
  ai: { includeFindings: false, redactMatches: true },  // mask, don't expose
};

Policies are normalized once (resolvePolicy) so a bare string like "block" and a full object both produce the same shape downstream, with per-mode flag defaults (block turns on blockOnHighRiskDevice and blockPromptInjection). The React layer holds the only ephemeral state — useDeviceIntegrity tracks report/loading/error with a mounted-ref guard so a check that resolves after unmount can't set state on a dead component.


11. Observability: explainability & reporting

SentinelRN's "observability" is inward-facing: instead of emitting telemetry, it makes every decision self-explaining. A ThreatReport carries the score, level, the full list of signals, the recommended action, and a timestamp. A PolicyDecision carries allowed, the mode, the recommended action, and a reasons: string[] array spelling out exactly why — e.g. "Risk level: high (score 71).", "Detected signals: root, hooking.", "Blocked by policy (mode: block)."

This is the anti-boolean stance made concrete: an engineer reading a blocked action in a log can reconstruct the entire decision without a debugger. Raw evidence and raw matches are opt-in, so the explainability never becomes a leakage vector.


12. Error model & fail-open behavior

Two principles govern failure:

  1. The SDK never crashes the host app. Native detector failures are caught in Kotlin/Swift and reported as "not detected"; provider failures are caught in the integrity orchestrator and degrade to a clean report.
  2. Errors are structured, not thrown into user space. The SentinelError shape (code, module, message, cause?) tags which subsystem failed (integrity | ai | policy | redaction | native) so callers can handle it uniformly.

The guiding rule: a security tool that takes down the application has a worse failure mode than the threats it's meant to catch.


13. Security posture

This section is intentionally balanced — strong foundations, with an honest hardening backlog.

Strengths

  • No data egress. Everything runs locally; no prompts, signals, or secrets ever leave the device through SentinelRN.
  • Secure-by-default config. Monitor mode, no raw evidence, redaction on — the easiest path is the safe path.
  • No secret storage. The SDK detects secrets; it never persists them.
  • Honest uncertainty. Probabilistic detection is labeled with confidence; the docs explicitly forbid "guaranteed"/"unhackable" framing.
  • Validated detection. Luhn (cards) and reserved-range checks (SSNs) reduce the false positives that erode trust in a security tool.

Hardening opportunities (documented, not hidden)

  • Detection is bypassable. Root/jailbreak/hooking detection is an arms race; advanced attackers can hide indicators. The SDK raises cost, it doesn't guarantee.
  • Injection heuristics are regex-based. They catch the obvious cases and will miss novel phrasings; they must be paired with server-side AI safety.
  • PII patterns are locale-biased. SSN/phone/card patterns lean US-centric and need expansion for global coverage.
  • No runtime attestation yet. Play Integrity / App Attest / DeviceCheck would add a hardware-backed signal the current filesystem heuristics can't.

14. Performance engineering

TechniqueWhereBenefit
Pure-function corerisk/, threat/, policy/, ai/No allocations beyond results; trivially memoizable and tree-shakeable
Single-pass detectiondetectCandidatesEach detector runs once over the input; overlaps resolved in-memory
Backward redactionapplyRedactionsReplacing from the end avoids re-indexing the string per match
lastIndex reset guardpattern detectorsShared global regexes are reset defensively to avoid stateful misses
Snapshot-once native bridgegetIntegritySnapshotOne JS↔native round-trip per check, not one per detector
ESM + tree-shakingtsup buildApps pull in only the namespaces they touch
Lazy provider readgetConfig() thunkConfig changes apply without rebuilding the integrity module

15. Build, release & tooling

  • Turborepo drives build, test, typecheck, and lint across the workspace with caching, so CI only re-runs what changed.
  • tsup emits ESM + CJS + declarations per package from a one-line config.
  • TypeScript strict with verbatimModuleSyntax keeps the type/runtime boundary explicit and the public .d.ts clean.
  • Biome is the single lint+format tool — no ESLint/Prettier split.
  • Changesets manages versioning and publish across @sentinelrn/{core,native,react}; a release workflow is guarded on NPM_TOKEN.
  • Expo example app (New Architecture enabled) is the manual end-to-end smoke test on real devices.

16. Testing strategy

  • Vitest, 88 cases across 12 suites. Coverage concentrates on the logic where a bug is most dangerous: the risk math, threat normalization, policy arbitration, detector precision, injection rules, and redaction correctness.
  • Property-style scoring checks. The noisy-OR aggregation is tested for monotonicity and saturation (more/worse signals never lower the score; it never exceeds 100).
  • Detector precision + recall. Each secret/PII pattern is tested against both true positives and crafted near-misses (e.g. invalid-Luhn numbers, reserved SSNs).
  • React hooks are tested under jsdom with @testing-library/react, including the unmount-during-check race.
  • Isolated instances (createSentinel) keep tests deterministic — no shared singleton state bleeding between cases.

17. API design system

SentinelRN treats its API as the design system. The rules:

  • One entry point, a few namespacesSentinelRN.integrity / ai / policy / redaction. Concepts, not loose utility functions.
  • Never return a bare boolean — every result explains what happened, how severe, how confident, and what to do.
  • Difficult to misuse — secure defaults, optional config, and a uniform PolicyDecision shape across both integrity and AI.
  • Honest namingrecommendedAction, compromised, confidence; no marketing absolutes.

The example app's small theme (cards, badges, a policy selector, an integrity card, a PromptGuard card) exists only to render these structures — proving the report shapes map cleanly onto a real UI.


18. Key decisions & tradeoffs (summary)

DecisionWhyTradeoff accepted
Structured reports over booleansApps need decisions, not flagsMore surface area to learn than isRooted()
One risk engine for device + AIConsistent meaning of severity everywhereScoring weights must serve two domains at once
Pure-TS core, injected detectorsTestability + extensibility without API churnReal detection requires the native package wired up
Noisy-OR scoringGraceful saturation, rewards corroborationWeights are hand-tuned heuristics, not learned
Fail-open orchestrationNever crash the host appA silent detector failure looks like a clean device
Secrets beat PII beat injection on overlapMost-sensitive classification winsOccasional under-reporting of a lower-priority overlap
Heuristic injection rulesShip useful coverage now, on-deviceMisses novel attacks; needs server-side backup
Monitor as the default policyObserve before blocking real usersOut-of-the-box, nothing is actually blocked

19. Known hardening items

A candid backlog of what would make SentinelRN production-grade beyond v0.1:

  1. Runtime attestation — Play Integrity API, Apple App Attest, DeviceCheck for hardware-backed signals.
  2. Frida / hooking depth — dedicated instrumentation-framework detection beyond the current heuristics.
  3. App-signature validation — detect repackaging via signing-cert checks on Android.
  4. Global PII coverage — non-US phone/ID/card formats and configurable locale packs.
  5. Injection robustness — move beyond regex toward layered/semantic detection, with explicit server-side guidance.
  6. SSL-pinning & secure-logging helpers — close the "sensitive data leakage" threats the model documents but the MVP doesn't yet cover.
  7. OWASP MASVS mapping — trace each feature to a recognized mobile-security control.
  8. Expo config plugin — one-step native install so registration can't be forgotten.

SentinelRN is risk-based by design: detection is probabilistic and explainable, never a guarantee. The client reduces risk; the server must enforce trust.