npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@burnt-labs/expo-satya-attest

v3.2.0

Published

SATYA-W mobile TLS app-attestation (App Attest + Secure Enclave / Play Integrity) backed by the Rust satya-sdk core

Readme

@burnt-labs/expo-satya-attest

Expo / React Native native module for SATYA mobile app attestation. The module uses the Rust satya-sdk / satya-ffi pipeline for artifact construction and verifier policy, while Swift and Kotlin provide platform-specific WebView capture, native HTTPS replay, hardware-backed signing, and platform attestation evidence.

Prerequisite: register your app for an app id

SDK 3.x requires an appId. Register your app at https://provider-registry.burnt.com (Apps module) with your iOS identity (Apple Team ID + bundle id) and/or Android identity (package name + Play app-signing SHA-256). After a Burnt admin approves the registration and grants your app its providers, configure the id in any one of:

  • transportConfig.appId on SatyaProviderAttestButton,
  • initSatyaAttest({ appId }) at startup, or
  • EXPO_PUBLIC_SATYA_APP_ID in the build environment.

The app id is public by design — verification is gated by OS attestation matching your registered identities plus a per-app provider allowlist, so possession of the id alone grants nothing, and store binaries ship no verifier secret. Without a configured app id a verifier-backed flow fails before the provider WebView opens with APP_ID_REQUIRED; a malformed id fails validation with CONFIG_INVALID. An unregistered or unapproved id fails at the verifier with APP_NOT_REGISTERED / APP_DISABLED, and an ungranted provider fails with PROVIDER_ACCESS_DENIED. See docs/MULTI_TENANT_VERIFICATION.md. Flows also fail with app_platform_not_configured when the app has no registered identity for the current platform, so register every platform you develop and test on.

Current Flow

The supported Expo flow is certificate-pinned native replay. Third-party apps can use the SDK-owned button component for the full managed flow:

  1. The app renders SatyaProviderAttestButton with a provider id and transport config.
  2. The SDK refreshes SPKI pin policy from the verifier, falling back to registry pins.
  3. The SDK opens an in-app react-native-webview modal for login and request discovery.
  4. The SDK waits for a provider request that matches template requestMatchRules. For html/text endpoints it surfaces the matched page load to the interceptor by re-issuing it as a credentialed fetch().
  5. Swift/Kotlin replay the trusted template URL natively with shared WebView cookies.
  6. The native client uses system trust without user roots and requires SPKI pins.
  7. Native extraction applies the endpoint's reveal rules to the pinned response — JSON paths for json bodies, and regex (regex/jsonRegex) over the raw body — producing the disclosed claims. The Rust core emits a native-pinned-observed SATYA artifact.
  8. The SDK stamps Expo package metadata onto the artifact envelope, verifies locally, optionally posts the artifact to satya-verifier-service, and returns the result through onComplete.

The earlier mock and plain webview-observed modes are no longer exposed through the high-level Expo client because they do not prove a provider response fetched through a pinned native TLS connection.

TeeLS Hosted Passthrough (Opt-In)

The SDK also has an isolated, opt-in TeeLS hosted flow. It uses a normal registry provider and the normal app/provider grant, but it does not run request capture, native replay, SATYA artifact construction, local SATYA verification, or verifier artifact submission. SATYA performs one provider-policy request before opening the WebView solely to confirm that the configured app is active and has access to the provider.

Enable the flow for exactly one provider/app pair at initialization:

import {
  initSatyaAttest,
  SatyaProviderAttestButton,
  type SatyaTeelsAttestation,
  type SatyaTeelsClaimData,
} from '@burnt-labs/expo-satya-attest';

initSatyaAttest({
  providerId: 'satya.teels.v1',
  appId: 'app_your-registered-app-id',
  teelsFlow: true,
});

function TeelsAttestButton() {
  const verifyTeelsResult = async (
    attestation: SatyaTeelsAttestation,
    claimData?: SatyaTeelsClaimData
  ) => {
    // Required: verify the TeeLS quote, freshness, and expected data binding
    // in your application or backend before trusting the result.
    // `claimData` is the first argument to TeeLS success, usually the
    // JSON-safe payload forwarded by the hosted provider.
  };

  return (
    <SatyaProviderAttestButton
      providerId="satya.teels.v1"
      transportConfig={{
        verifierUrl: 'https://satya-verifier-v2-staging.burnt.com',
        appId: 'app_your-registered-app-id',
      }}
      onTeelsComplete={verifyTeelsResult}
      onError={(error) => console.error(error.code, error.message)}
    />
  );
}

The provider's existing login.url opens in the SDK WebView and its normal Registry injection adapts the hosted page to the SDK-owned bridge. A page that already uses the standard TeeLS callbacks and EarnOS success event/snapshot does not need frontend changes. See Provider Config for the adapter and device-validation contract.

Only a non-null, JSON-safe attestation object can complete the flow. If provided, claimData is additionally forwarded as a second argument to onTeelsComplete. Success is returned only through onTeelsComplete; invalid/null data, hosted failures, timeout, WebView failure, and access denial use the normal onError path. Closing the sheet before a terminal result uses onCancel.

Native-replay-only props are not used by TeeLS. A component may keep an onComplete handler for normal mode, but TeeLS never calls it. Initialize the SDK before mounting the button. A later init with teelsFlow: false or with the option omitted disables TeeLS for subsequently mounted buttons; normal providers continue to use the existing native replay flow unchanged.

The returned object is an opaque, unverified TeeLS result and is delivered as the first argument. claimData is the hosted-provider payload whose inner data object is hashed into the quote's report_data; treat both as untrusted input until verified. Do not treat callback delivery as a SATYA verification claim, and do not log or persist the result by default.

Verifying the TeeLS result

The SATYA SDK performs no verification of the TeeLS result — it neither verifies the quote nor checks that the claim data matches it. Before trusting the result, the consuming application or its backend must verify it:

  1. DCAP quote. attestation.quote is a hex-encoded Intel TDX quote. Post it to the independent TeeLS verifier service and require success: true, quote.verified: true, and a tcb_status your policy accepts:

    curl -X POST https://attest.burnt.com/verify \
      -H 'content-type: application/json' \
      -d '{"hex":"<quote hex>"}'
  2. Measurements. Compare the verified quote's mrtd and rtmr0rtmr3 against the expected-measurements registry published independently by the verifier (GET https://attest.burnt.com/measurements); require a match against one of its valid[] sets.

  3. Data binding. Use reportdata from the verified quote response (never the advisory attestation.reportDataHex field — it is server-supplied). Its first 32 bytes must equal SHA-256(JSON.stringify(data)) over the exact result-data object the page delivered (in this flow, claimData.data; remaining bytes are zero-padded). When the provider binds a policy hash, the envelope is { data, policyHash } instead of bare data. Any mismatch means the claim data was substituted after attestation.

The result carries no nonce, so enforce your own freshness and replay policy wherever the attestation crosses a trust boundary.

Native Trust Tiers

| Platform | Witness key | Platform evidence | Typical tier | | ---------------- | ------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------- | | iOS device | Secure Enclave P-256 | App Attest assertion/attestation | ios-app-attest-sep-bound | | iOS simulator | software P-256 | honest simulator fallback | ios-simulator-dev | | Android device | Keystore EC, StrongBox when available | Play Integrity hook | android-strongbox-play-integrity / android-keystore-play-integrity-no-strongbox | | Android emulator | software EC | honest emulator fallback | android-emulator-dev |

Artifacts are signed by the app/witness path, not by a separate signing service. Production-grade verification relies on canonical statement hashes, device/witness signatures, active provider policy, fresh revocation epochs, backend-trusted SPKI pins, and pass-2 platform verification where appropriate.

Artifact Identity And Versioning

Every artifact includes a stable attestationId. It is derived from the signed statement hash, so a replay of the same artifact keeps the same id while a fresh attestation gets a new one.

New SDK artifacts also include envelope metadata:

{
  "schemaVersion": "1.1.0",
  "attestationId": "satya_w_...",
  "attestationIdVersion": "statement-sha256-96-v1",
  "producer": {
    "sdk": "@burnt-labs/expo-satya-attest",
    "sdkVersion": "3.1.0",
    "artifactSchemaVersion": "1.1.0",
    "coreVersion": "0.1.0"
  }
}

The verifier uses this metadata for compatibility routing and diagnostics. It is not allowed to downgrade cryptographic checks; policy decisions still come from the signed statement, provider registry policy, and platform evidence.

Build The Rust Core

The generated native artifacts are gitignored:

  • ios/Frameworks/
  • ios/Generated/
  • android/src/main/jniLibs/
  • generated uniffi Swift/Kotlin glue

Build them from the workspace root before opening Xcode or Gradle:

# iOS XCFramework + Swift glue
platform_sdks/expo-satya-attest/scripts/build-rust-ios.sh

# Android .so files + Kotlin glue
# One-time prereqs: Android NDK, JDK 17, cargo-ndk, and Android Rust targets.
platform_sdks/expo-satya-attest/scripts/build-rust-android.sh

From the module directory, the same commands are available as:

npm run build:native:ios
npm run build:native:android

build:native:ios strips local/debug symbols from the XCFramework. Local builds include the physical-device arm64 iOS slice by default; publishing builds set SATYA_IOS_INCLUDE_SIMULATOR=1 so simulator fallback slices ship too. See PUBLISHING.md for release and binary-size knobs.

Build TypeScript after editing src/*.ts or src/*.tsx:

cd platform_sdks/expo-satya-attest
npm install
npm run build
npm run lint

The example app consumes the compiled build/ directory, so restart Metro with --clear after rebuilding the module.

Provider Registry

Provider configs are loaded from the Provider Registry API at runtime. The SDK default is https://api.provider-registry.burnt.com, and apps can override it once at startup:

import { initSatyaAttest } from '@burnt-labs/expo-satya-attest';

initSatyaAttest({
  providerId: 'kaggle.current_user.v1',
  providerRegistryApi: process.env.EXPO_PUBLIC_PROVIDER_REGISTRY_API,
  logLevel: 'warn',
});

The SDK fetches /api/providers/{providerId} and fails closed if the provider is unavailable, inactive, or unsafe to execute on-device. The registry service owns provider-schema governance; the SDK only normalizes fields it needs at runtime and enforces mobile safety checks such as HTTPS URLs, SPKI pins, request matching, and reveal parsing. The raw provider schema is documented in PROVIDER_CONFIG.md; runtime loading rules, errors, and logging are documented in PROVIDER_REGISTRY.md.

Useful exports:

import {
  initSatyaAttest,
  loadProviderTemplate,
  SatyaProviderAttestButton,
  validateProviderTemplate,
  withProviderSpkiPins,
  SatyaMobileClient,
} from '@burnt-labs/expo-satya-attest';

Load the configured provider for custom diagnostics or a fully custom UI:

const providerTemplate = await loadProviderTemplate();

Native replay requires at least one SPKI SHA-256 base64 pin at runtime. By default, SatyaProviderAttestButton fetches GET /v1/providers/{providerId}/policy from transportConfig.verifierUrl before opening the WebView, validates that the returned provider id, host, and policy hash match the selected template, and freezes those pins for the attestation run. For a local verifyBackend={false} flow, an unavailable verifier can fall back to the SDK registry pins. Set providerPolicyRefresh="verifier-required" to disable that fallback, or providerPolicyRefresh="template-only" for offline demos. When verifyBackend is enabled, every mode—including template-only and caller-supplied spkiPins—still completes the app/provider access preflight before opening the WebView; an unavailable verifier blocks that attempt as retryable. Only a fully local verifyBackend={false} flow skips the verifier gate.

Installation Options

Every install path also needs the WebView and safe-area peer dependencies:

npx expo install react-native-webview react-native-safe-area-context

Use npm for released apps:

npx expo install @burnt-labs/expo-satya-attest

Use a local checkout while developing the SDK or testing unpublished changes:

cd /path/to/tls-app-attest/platform_sdks/expo-satya-attest
npm install
npm run build
npm run check:native-artifacts

cd /path/to/your-expo-app
npm install /path/to/tls-app-attest/platform_sdks/expo-satya-attest
npx expo install react-native-webview react-native-safe-area-context

Re-run npm run build in the SDK directory after TypeScript edits. Re-run the native build scripts after Rust/UniFFI edits so the local package still contains ios/Generated, ios/Frameworks, Android jniLibs, and generated Kotlin.

Use GitHub when you want a pinned unreleased version. The most reliable GitHub install is a release tarball produced by npm pack, because it contains the same files npm would publish:

npm install https://github.com/<owner>/<repo>/releases/download/expo-satya-attest-v3.1.0/burnt-labs-expo-satya-attest-3.1.0.tgz
npx expo install react-native-webview react-native-safe-area-context

Installing from a Git ref also works when that ref has the SDK package at the repository root, such as a dedicated SDK repo or a subtree/split branch:

npm install github:<owner>/<sdk-repo>#v3.1.0
npm install git+https://github.com/<owner>/<sdk-repo>.git#v3.1.0
npx expo install react-native-webview react-native-safe-area-context

Do not point npm at the monorepo root unless that Git ref is prepared so package.json, build/, native artifacts, and Expo module files are at the root of the Git package. npm does not install this monorepo's platform_sdks/expo-satya-attest subdirectory directly from a normal GitHub URL.

After changing the install source, rebuild the native app:

npx expo prebuild --clean
npx expo run:ios --device
npx expo run:android

Plug-And-Play API

Add the Expo config plugin as shown below, rebuild the native app, then render the SDK-owned button:

import { initSatyaAttest, SatyaProviderAttestButton } from '@burnt-labs/expo-satya-attest';

initSatyaAttest({
  providerId: 'kaggle.current_user.v1',
});

export function KaggleAttestation() {
  return (
    <SatyaProviderAttestButton
      providerPolicyRefresh="verifier-first"
      transportConfig={{
        verifierUrl: 'https://satya-verifier.example.com',
        cloudProjectNumber: 0,
      }}
      verifyBackend
      buttonLabel="Open Kaggle"
      onComplete={(result) => {
        saveSatyaArtifact(result.raw);
        handleSatyaVerification(result.verify, result.backendVerify);
      }}
      onError={(error) => {
        reportSatyaError(error);
      }}
    />
  );
}

When pressed, the component opens the provider WebView inside the same app, captures the matching request, performs native replay with cookie sharing, verifies locally, posts to the backend verifier when verifyBackend is set, and returns a SatyaProviderAttestationResult.

Apps can customize the button label, modal title, WebView props, and callbacks. Apps that use a custom provider allow-list can pass providerTemplate instead of loading the configured provider id. Explicit spkiPins are still supported for controlled tests and override only the TLS pin source; verifier-backed flows still preflight app/provider access before opening the WebView. The managed WebView uses provider userAgent settings when present, otherwise it falls back to a platform-matched mobile browser user agent (Safari on iOS, Chrome on Android); pass webViewUserAgent to override it explicitly.

Presentation

By default the provider WebView opens as an in-app sheet that keeps the host app visible behind it, so the flow reads as part of the app rather than a separate browser window. On iOS this is the native pageSheet (rounded card, interactive swipe-to-dismiss); on Android it is an animated bottom sheet with a scrim and drag grabber. The sheet is presented immediately on tap and the provider policy resolves behind a skeleton, so there is no blank pause before the sheet appears. The header shows a browser-style secure-host pill (lock icon + domain), a reload button, and a page-load line that advances while a page loads and clears when it finishes; both the main page and the managed OAuth popup fade in over their first load instead of flashing white.

Once the provider's injection confirms the session is authenticated (the __satyaReady login signal), the sheet conceals the provider page behind a "Verifying your account…" cover while capture and native replay continue underneath, so users see only the login window and never the SDK driving the authenticated site. The cover is presentation-only (capture gating and verification are unchanged); if capture does not start within a short watchdog window it offers a "Return to provider" path that keeps the flow alive. Providers whose injection does not emit __satyaReady keep the previous behavior and are covered from request capture onward. See PROVIDER_CONFIG.md for the signal contract.

Pass presentation="fullScreen" to keep the legacy full-window takeover. Set prefetch to resolve the provider policy and warm the WebView engine at mount so the sheet opens straight into the loading login page (it trades an eager registry/policy round-trip on mount for a faster first open). The sheet's look is tunable through theme:

<SatyaProviderAttestButton
  presentation="sheet" // default; use "fullScreen" for the legacy full-window modal
  prefetch // resolve policy + warm the WebView at mount for an instant first open
  theme={{
    accentColor: '#2752d9',
    sheetCornerRadius: 20, // top corner radius of the sheet (default 16)
    backdropColor: 'rgba(9, 14, 26, 0.45)', // Android scrim behind the sheet
    showGrabber: true, // drag grabber at the top of the sheet
  }}
  transportConfig={{ verifierUrl: 'https://satya-verifier.example.com', cloudProjectNumber: 0 }}
/>

Injection, request capture, native replay, and OAuth popup handling are identical in both presentation modes; only the container around the WebView changes. Keyboard handling is also identical: on iOS the modal content automatically insets by the exact keyboard overlap when the keyboard appears (WKWebView does not resize its layout viewport on its own), so provider pages that pin their primary button to the bottom of the screen stay reachable while typing; Android relies on the standard adjustResize behavior.

Error Diagnostics

SDK failures are reported as SatyaError. The error has a stable code, an inferred stage, a per-occurrence errorId, and a redacted diagnostics object that can be forwarded to app telemetry. Native Expo promise rejections are preserved under diagnostics.native when the platform exposes a native code or domain.

import { SatyaError } from '@burnt-labs/expo-satya-attest';

function reportSatyaError(error: SatyaError) {
  telemetry.capture('satya_attestation_failed', error.diagnostics);
}

Diagnostics reach your app through two channels:

  • onError(error) receives the SatyaError itself — read error.code, error.stage, error.errorId, or forward error.diagnostics.
  • onProgress(progress) carries the same payload on the terminal failed phase as progress.error (typed SatyaErrorDiagnostics), so progress-driven UIs can capture diagnostics without a separate onError handler.
import type { SatyaErrorDiagnostics } from '@burnt-labs/expo-satya-attest';

<SatyaProviderAttestButton
  // ...
  onProgress={(progress) => {
    if (progress.phase === 'failed' && progress.error) {
      telemetry.capture('satya_attestation_failed', progress.error);
    }
  }}
  onError={(error) => reportSatyaError(error)}
/>;

JSON.stringify(error) emits the same diagnostics payload. The SDK redacts sensitive context keys such as cookies, authorization headers, tokens, credentials, and request or response bodies before they appear in diagnostics.

SatyaMobileClient.verify() also wraps native verifier rejections as SatyaError. Consumers that previously branched on a native rejection code should inspect error.diagnostics.native?.code or error.diagnostics.cause?.code.

Diagnostics payload

error.diagnostics (type SatyaErrorDiagnostics) is the stable, redacted shape to log or forward to telemetry:

| Field | Type | Notes | | --- | --- | --- | | errorId | string | Per-occurrence id (satya_<time>_<rand>); correlate logs to a single failure. | | code | SatyaErrorCode | Stable machine-readable code (see below). | | stage | SatyaErrorStage | Coarse pipeline bucket (see below). | | message | string | Human-readable summary (not redacted — don't put secrets in messages). | | context? | object | Operation context, recursively redacted (sensitive keys → [redacted Nb]). | | native? | { code?, domain?, message?, name? } | Present when the cause is a native Expo rejection exposing a code/domain. | | cause? | { code?, message?, name? } | Present when an underlying error was wrapped. |

stage is inferred from code, though some operations refine it (e.g. a native module init failure reports stage: 'initialization' while keeping code: 'SATYA_NATIVE_REPLAY_FAILED'). Use code for precise branching and stage for dashboards/alerting.

stage values: initialization, configuration, providerRegistry, providerPolicy, providerTemplate, webViewCapture, nativeReplay, artifactVerification, backendVerification, validation, unexpected.

code values (grouped by the stage they normally map to):

| Stage | Codes | | --- | --- | | configuration | SATYA_CONFIG_INVALID, SATYA_VERIFIER_URL_INVALID | | providerRegistry | SATYA_PROVIDER_ID_REQUIRED, SATYA_PROVIDER_NOT_FOUND, SATYA_REGISTRY_FETCH_FAILED, SATYA_REGISTRY_INFO_MISMATCH, SATYA_REGISTRY_INVALID | | providerPolicy | SATYA_PROVIDER_POLICY_FETCH_FAILED, SATYA_PROVIDER_POLICY_INVALID, SATYA_PROVIDER_POLICY_MISMATCH | | providerTemplate | SATYA_PROVIDER_ENDPOINT_INVALID, SATYA_PROVIDER_RULE_INVALID, SATYA_PROVIDER_TEMPLATE_INVALID, SATYA_PROVIDER_URL_INVALID | | webViewCapture | SATYA_PROVIDER_CAPTURE_INVALID, SATYA_PROVIDER_LOCATION_PERMISSION_DENIED | | nativeReplay | SATYA_NATIVE_REPLAY_FAILED | | artifactVerification | SATYA_ARTIFACT_VERIFICATION_FAILED | | initialization | SATYA_UNSUPPORTED_PLATFORM | | validation | SATYA_VALIDATION_FAILED | | unexpected | SATYA_UNEXPECTED |

SatyaError, SatyaErrorCode, SatyaErrorDiagnostics, and SatyaErrorStage are all exported from the package root.

App-Owned Buttons And Auto-Start

Apps with their own connect button can mount the same managed flow when they are ready to open the WebView. In this mode, the app owns the visible button and the SDK owns the WebView, capture, native replay, and verification flow:

export function AppOwnedConnectButton() {
  const [openSatya, setOpenSatya] = useState(false);

  return (
    <>
      <Pressable onPress={() => setOpenSatya(true)}>
        <Text>Connect Kaggle</Text>
      </Pressable>

      {openSatya ? (
        <SatyaProviderAttestButton
          autoStart
          renderButton={false}
          transportConfig={{
            verifierUrl: 'https://satya-verifier.example.com',
            cloudProjectNumber: 0,
          }}
          verifyBackend
          onComplete={(result) => {
            setOpenSatya(false);
            saveSatyaArtifact(result.raw);
          }}
          onCancel={() => setOpenSatya(false)}
          onError={(error) => {
            setOpenSatya(false);
            reportSatyaError(error);
          }}
        />
      ) : null}
    </>
  );
}

autoStart opens once per component mount. To start another flow, unmount and remount the component when the app-owned button is pressed again. To open without a user pressing a visible SATYA button, set the state from your own route, onboarding step, deep link, or effect, then render the hidden launcher:

{shouldOpenSatya ? (
  <SatyaProviderAttestButton autoStart renderButton={false} {...satyaProps} />
) : null}

Composite Proofs

A composite proof attests claims from several governed endpoints gathered in one authenticated provider session. Strict-all artifacts are schema 2.0.0 (legacy) or app-bound 2.1.0; signed partial-endpoint artifacts use 2.2.0. Instead of a single endpointId, the app passes a proofPlanId — a registry-defined proof plan that selects an ordered set of 1–8 required endpoint ids. The app never supplies URLs or an endpoint list; every endpoint the plan replays is a pre-approved endpoints[] entry, and the plan is folded into the provider policyHash.

Provider-selective partial endpoints

Some providers cannot guarantee that every proof-plan endpoint fires in every session. An app may opt exact provider ids into partial completion without changing provider or proof-plan config:

initSatyaAttest({
  partialEndpointProviderIds: ['provider.identity.v1'],
});

Matching is exact and case-sensitive; entries are never treated as patterns. An empty or omitted list keeps every provider strict-all. There is no matching Provider Registry, Devtool, or verifier setting. The SDK list is an app availability preference; the backend remains the security authority.

An authenticated __satyaReady signal starts the capture window and releases readiness-gated endpoints. Without it, the first accepted ungated capture starts the window. The capture layer attempts the full plan until maxDurationMs. Full capture success stays on the existing strict-all path. Otherwise, it sends the confirmed 2xx request subset to native replay, which requires at least one successful exchange and may further omit availability, timeout, non-2xx, or missing-claim failures. TLS trust, SPKI, origin, request-policy, and binding failures remain fatal.

  • the signed set must be a non-empty proper subset in proof-plan order;
  • every claim-equality account-binding endpoint must be present in the final subset;
  • schema 2.2.0 signs completionMode: "partial-endpoint-subset"; the semantic hash, exchange root, and domain-separated proof challenge bind the exact ordered endpoint IDs;
  • schemas 2.0.0 and 2.1.0 have no completion mode and remain strict-all;
  • the verifier directly compares the signed subset with the current full registry proof plan and fully verifies every included exchange;
  • both the managed component and public SatyaMobileClient API require successful backend verification for schema 2.2.0, even when verifyBackend is false;
  • native/local verifyArtifact* returns backend-required for schema 2.2.0 and is not an acceptance verdict;
  • the verifier verdict always distinguishes strict-all from partial-endpoint-subset and returns endpointIds, omittedEndpointIds, and planComplete;
  • relying parties that require full completion must require planComplete: true (equivalently, completionMode: "strict-all" and an empty omittedEndpointIds).

The verifier proves that every presented exchange is valid and belongs to the configured plan. It cannot prove that the omitted endpoints were actually attempted; that is SDK execution behavior, not a property recoverable from a subset proof.

If the window expires without a successful endpoint, capture fails with no-endpoints-captured. Timeout diagnostics list unconfirmedEndpointIds.

proofPlanId is mutually exclusive with endpointId — pass exactly one. It works on the same SatyaProviderAttestButton and low-level client:

Providers without proofPlans[] remain single-endpoint compatible when no plan is requested. Requesting a proofPlanId from a provider with no plans, or requesting an unknown plan id, is rejected instead of silently changing the requested policy.

<SatyaProviderAttestButton
  providerId="provider.example.v1"
  proofPlanId="account-summary" // mutually exclusive with endpointId
  providerPolicyRefresh="verifier-first"
  transportConfig={{ verifierUrl: 'https://satya-verifier.example.com', cloudProjectNumber: 0 }}
  verifyBackend
  onComplete={(result) => {
    // result.proofPlanId, result.capturedRequests (every request in plan order),
    // result.capturedRequest aliases the first entry for backward compatibility.
    saveSatyaArtifact(result.raw);
  }}
/>

The SDK captures every required request inside the one WebView session, then native code (iOS Swift / Android Kotlin) replays each endpoint sequentially over a fresh SPKI-pinned connection using a proof-local cookie jar (seeded once from the WebView store, never written back; cookie contents are never serialized or hashed), with redirects disabled and only 2xx responses accepted. onProgress reports proofPlanId, endpointId, exchangeIndex, and exchangeCount as each exchange runs.

Limits

  • 1–8 endpoints per plan, ordered; the order is order-sensitive and participates in policyHash.
  • Per-response 8 MiB and aggregate 16 MiB response-byte caps across the plan.
  • Capture selection runs for at most the plan's maxDurationMs (an integer in [1000, 120000] ms). A readiness signal starts the window when received; otherwise the first accepted capture starts it. Later readiness never resets an earlier start.

What a v2 artifact contains

The Rust core emits schema 2.0.0 for legacy strict-all, 2.1.0 for app-bound strict-all, or 2.2.0 for a signed endpoint subset. Each artifact carries, per witnessed exchange, its own TLS/handshake commitments, request/response commitments, record-log evidence, and endpoint-scoped disclosed claims (the same alias may repeat across different endpoint scopes; endpoint provenance is preserved). All exchanges are bound by one domain-separated, order- and count-bound aggregate exchangeCommitmentRoot signed by the device key: omitting, duplicating, reordering, or mutating any exchange changes the root. The device statement signature covers the aggregate root, proof-plan id, policy hash, exchange count, freshness window, revocation epoch, and account-binding evidence.

Account-binding assurance

Every plan declares how it proves the endpoints belong to one account, surfaced on the backend verdict as accountBindingAssurance:

  • shared-session-attested — all exchanges ran under the one attested on-device session (the plan declares this as accountBinding.mode: "shared-session"); no cookie hashing is involved.
  • claim-equality — named claims from ≥2 endpoint scopes must be equal; the verifier compares the canonical claim values. Claims stay endpoint-scoped.

The account-binding evidence is a bare attested marker plus claim references only — it never carries cookie values, cookie hashes, authorization values, or session tokens.

Pass-2 platform binding

For v2 artifacts the pass-2 evidence binds to the prepared composite proof challenge: the App Attest assertion clientDataHash = SHA256(proofChallengeDigestHex ASCII), and the Play Integrity nonce = base64url(SHA256("satya-w-playintegrity-bind-v2" || 0x00 || proofChallengeDigestHex ASCII)). Composite pass-2 policies reject a v2 artifact that carries only key-enrollment evidence. Single-endpoint schema-1 binding is unchanged.

v1 / v2 compatibility

Single-endpoint schema-v1 verification remains fully available; endpointId still emits a schema-v1 native-pinned-observed artifact, and proofPlanId emits schema v2. This SDK release can emit both (dual-schema support since 2.0.0). The backend dispatches on schema major before full deserialization (ParsedArtifact::{Single,Composite}): schema 1 selects the preserved single-artifact profile for SDK 1–3, and schema 2 selects expo-sdk-2-composite for SDK 2–3. The backend loads the provider, plan, endpoints, reveal allowlists, SNI, pins, freshness, and revocation policy independently from the registry (never from the artifact), recomputes the provider's canonical policyHash, requires the artifact's exact hash, and returns a typed verdict with proofPlanId, completionMode, endpointIds, omittedEndpointIds, planComplete, exchangeCount, and accountBindingAssurance. Strict-all verification rejects any missing, duplicated, unknown, reordered, failed, or policy-mismatched exchange. Partial mode applies the same checks to every endpoint in the exact signed subset and validates that subset against the registry's full proof plan.

Tier and activation gate

Like single-endpoint proofs, composite production evidence still requires physical-device validation (real App Attest / Secure Enclave / Play Integrity / Keystore / WebView cookie sharing). Simulator/emulator results are DEVELOPMENT tier and are never production evidence. The Provider Registry validates proofPlans[] and folds the canonical plan into policyHash. The proofPlans[] registry schema is documented in PROVIDER_CONFIG.md §8.1.

Low-Level API

import {
  initSatyaAttest,
  loadProviderTemplate,
  SatyaMobileClient,
  withProviderSpkiPins,
} from '@burnt-labs/expo-satya-attest';

initSatyaAttest({
  appId: 'app_00000000-0000-4000-8000-000000000000',
  providerId: 'kaggle.current_user.v1',
});

const providerTemplate = withProviderSpkiPins(await loadProviderTemplate(), [
  'KWQsjul7mJDk+i6fAisGLhWLd43FUs5ayVmkz5Y54DU=',
]);

const client = new SatyaMobileClient();
const { artifact, raw, verify } = await client.attestNativeReplay({
  providerTemplate,
  capturedRequest,
  transportConfig: {
    verifierUrl: 'http://127.0.0.1:7047',
    cloudProjectNumber: 0,
  },
});

capturedRequest should come from the WebView interceptor and must match the selected provider template. The returned raw string is the artifact JSON to post to POST /v1/artifacts/verify on satya-verifier-service.

Native replay artifacts are compact by default: signed public values live in artifact.semanticStatement.disclosedClaims with UI-friendly name aliases and audit-friendly sourcePath JSON paths. Legacy/full artifacts may still include a top-level disclosure object, but new native replay artifacts omit that duplicate surface. Operational telemetry/logging metrics such as latency, byte counters, and backend labels are also omitted from new proof artifacts.

Config Plugin

Add the plugin to inject App Attest entitlements and platform pinning config:

{
  "expo": {
    "plugins": [
      [
        "@burnt-labs/expo-satya-attest",
        {
          "appAttestEnvironment": "development",
          "pinnedProviders": [
            {
              "host": "www.kaggle.com",
              "spkiSha256B64Pins": ["KWQsjul7mJDk+i6fAisGLhWLd43FUs5ayVmkz5Y54DU="]
            }
          ]
        }
      ]
    ]
  }
}

Use development for debug/TestFlight and production for App Store builds. iOS App Attest requires a real device plus an App ID with the DeviceCheck/App Attest capability.

enableWebViewGeolocation is off by default. Enable it only when a registry provider uses requiresLocation: true; the plugin then adds the iOS usage description and Android fine/coarse location permissions. On Android, the SDK requests runtime access after resolving an opted-in provider and before loading its WebView. A denial reports SATYA_PROVIDER_LOCATION_PERMISSION_DENIED. Providers without requiresLocation never trigger the permission request or receive WebView geolocation.

{
  "enableWebViewGeolocation": true,
  "locationUsageDescription": "Location is used to verify provider eligibility."
}

The plugin also accepts cleartextHosts, an array of hosts (or URLs — only the hostname is kept) allowed to use plain-HTTP on Android via the generated network security config. Use it only for development setups where the example app must reach a LAN verifier over http://; provider pin entries always keep cleartext disabled:

{
  "appAttestEnvironment": "development",
  "cleartextHosts": ["192.168.1.50"]
}

Example App

cd platform_sdks/expo-satya-attest/expo-satya-attest-example
npm install --ignore-scripts
mkdir -p node_modules/@burnt-labs
ln -sfn ../../.. node_modules/@burnt-labs/expo-satya-attest
npx expo run:ios --device
npx expo run:android

See expo-satya-attest-example/README.md for device env setup and backend verifier commands.

Verification Backend

SDK 3 staging verification uses the multi-tenant https://satya-verifier-v2-staging.burnt.com deployment: configure your public app id (EXPO_PUBLIC_SATYA_APP_ID / transportConfig.appId, from the Satya devtool) — there is no bearer token. Schema 0/1 artifacts stay on the single-artifact verifier and schema 2/2.1 selects the composite verifier; the service requires pass-2 for real devices and preserves /verify and /v1/verify for older clients alongside the canonical /v1/artifacts/verify route. Older SDK 1.x/2.x builds keep using the frozen legacy https://satya-verifier-v1-staging.burnt.com deployment (bearer-authenticated, legacy-v1 branch) during migration.

The top-level producer block drives compatibility routing but is not witness-signed. Cryptographic acceptance comes from the signed statement, registry policy, TLS/SPKI evidence, and App Attest or Play Integrity.

Run the SATYA verifier service from the workspace root:

SATYA_ALLOW_PLATFORM_PASS1=true cargo run -p satya-verifier-service

The verifier loads provider policy from the Provider Registry API and does not require a separate signing-service public-key bundle.

SDK verifier URLs must use HTTPS, except loopback HTTP for local development (http://localhost, http://127.0.0.1, or http://[::1]). For Android physical-device testing against a laptop verifier, use adb reverse tcp:7047 tcp:7047 and set EXPO_PUBLIC_VERIFIER_URL=http://127.0.0.1:7047; otherwise expose the verifier through an HTTPS tunnel or deployed HTTPS endpoint.

Provider Health Monitoring

Every native replay reports one anonymous health ping to the Burnt provider monitor (POST https://provider-monitor.burnt.workers.dev/ingest) so per-provider success rates and proof-generation latency show up on the shared dashboard. This is on by default and needs no app configuration or token.

  • outcome: "ok" fires as soon as the attestation artifact is produced, with latencyMs measured from captured-request handoff to artifact creation.
  • outcome: "failed" fires when the native replay pipeline fails to produce an artifact (no latency attached).
  • Artifact verification — the embedded check and the backend verifier — is outside the ping: its result depends on device trust (App Attest / Play Integrity) and verifier policy, so it never changes the reported outcome and is tracked on the verifier's own monitor rows.

The ping is fire-and-forget: it runs concurrently with verification, times out after 10 s, and can never block, slow down, or fail an attestation. The payload is only provider, outcome, and latencyMs — no user data, claims, session identifiers, or artifact contents. The endpoint enforces a registry allow-list; unknown provider ids are acknowledged but not recorded.

Checks

cd platform_sdks/expo-satya-attest
npm run build
npm run lint
npm run release:check

cd expo-satya-attest-example
./node_modules/.bin/tsc --pretty false --noEmit

cd ../../..
cargo test -p satya-ffi native_replay_observed_nested_json_paths_verify
cargo test -p satya-verifier-service

Publishing

The Expo SDK should publish to npm. The npm package must include the generated Android .so files, generated UniFFI Kotlin/Swift glue, and iOS XCFramework so third-party apps can install it without building Rust locally. See PUBLISHING.md for the release checklist, dry-run commands, binary-size guidance, and scoped-package naming recommendations.

Security Notes

  • WebView response bytes are not attested. The WebView is only a login/request discovery surface.
  • The managed WebView bridge currently treats SDK-injected request, response, bearer, and login-ready messages as control-plane signals. Native replay still fetches the trusted template URL with system TLS and SPKI checks, but apps should treat provider account binding as a backend responsibility when bearer/session state matters.
  • Cookies are shared into native replay through platform cookie stores.
  • Native replay blocks user roots and requires client-side SPKI pins so the replay connection fails closed before producing an artifact.
  • The SDK never learns production trust by fetching the provider's live certificate and trusting it for the same run. Dynamic pins come from verifier policy, are validated against the selected template, and are frozen before WebView capture starts.
  • Backend verification independently re-derives trusted policy hashes and pins from backend config, not from the client artifact alone. Client pins protect the fetch; backend pins protect acceptance.
  • For production deployments, serve verifier policy over an authenticated channel controlled by the verifier operator. Raw provider SPKI discovery is only suitable as a manual/dev maintenance aid, not as automatic trust.
  • Development artifacts remain dev-tier and should not be accepted by production verifiers.