Skip to content

Latest commit

 

History

History
354 lines (289 loc) · 15.4 KB

README.md

File metadata and controls

354 lines (289 loc) · 15.4 KB

Maple SDKs

This directory contains the TypeScript/React and Rust clients used by Maple and internal OpenSecret applications. Both clients establish attested, end-to-end encrypted sessions with an OpenSecret backend and expose the API surface needed by those applications.

The developer/platform API remains part of the TypeScript SDK for internal OpenSecret workflows. This repository does not maintain or deploy a separate documentation website; keep behavior documentation close to the exported code and tests.

Repository layout

  • src/ — @mapleai/sdk, including the React providers, encrypted API client, attestation policy, model/conversation APIs, and internal developer platform client.
  • rust/ — the maple-sdk crate, imported as maple_sdk by native clients.
  • docs/PLATFORM.md — internal developer/platform API notes.
  • the sdk-ts, sdk-rust, and deps-sdk lanes in the root scripts/ci/lanes.py — TypeScript, Rust, and supply-chain validation for this directory, run by the root ci.yml workflow.

Maple's own Rust consumers (proxy, apps/maple-agent, the Research native shell) build the in-tree Rust SDK through path dependencies, so SDK changes are usable in this repository as soon as they merge; publication serves external users. TypeScript consumers may use a file: dependency the same way or pin a published version. The consumer's manifest and lockfile determine what it builds. Research uses the Rust SDK on desktop and mobile for native authentication; the embedded proxy remains desktop-only. See the consumer version policy for switching sources and preparing client releases.

Package identity migration

The package names are @mapleai/sdk and maple-sdk, first published at TypeScript version 3.5.2 and Rust version 3.6.2. Their versions and publication remain independent of Maple application releases.

The rename preserves the exported API, including OpenSecretProvider, useOpenSecret, OpenSecretDeveloper and OpenSecretClient. OpenSecret remains the backend name. Backend URLs, configuration variables, signed-PCR verification and encrypted transport retain their existing contracts. Existing published @opensecret/react and opensecret packages remain available to older consumers.

Version 4 upgrade

Both SDKs use Transport V2 starting at 4.0.0. Version 4 requires a backend with Transport V2 support and does not fall back to V1. Applications upgrading from V1 must establish a new session and ask existing users to sign in again. Native authentication and hosted OAuth callbacks must use the matching V2 flows; updating only one side of that handoff is insufficient.

The TypeScript SDK, Rust SDK, and Maple app are published independently. package.json and rust/Cargo.toml declare the in-tree SDK versions; each consumer's manifest and lockfile declare the version and source it actually builds. Editing SDK source does not change a registry-pinned consumer; use local links when validating an SDK change with that consumer. See the publishing guide.

Security model

For non-local endpoints, both SDKs require HTTPS, verify AWS Nitro attestation, and enforce an environment-scoped PCR0 trust policy before completing key exchange. The SDKs bundle environment-specific PCR0 trust roots and the verification key used to authenticate signed remote history entries.

Mock attestation is limited to exact loopback development endpoints (plus the documented Android emulator alias in the Rust SDK). Do not weaken attestation, PCR0 validation, or encrypted transport to accommodate a caller.

Certificate validity is checked against the device clock, as AWS specifies, after the chain and COSE signatures are verified. notBefore gets a five-minute leeway (ATTESTATION_NOT_BEFORE_LEEWAY_MS in both SDKs) because the enclave leaf certificate is issued without backdating and re-issued about every 2 h 45 m, so a device clock a few seconds slow used to fail right after each re-issue. notAfter is strict: a fast device clock is bounded by the leaf's remaining validity (15 minutes to 3 hours), and freshness itself comes from the per-handshake nonce. A failure raises AttestationClockSkewError, whose message compares the device clock with the document's signed timestamp and tells the user to check the device's date, time and time zone; the OAuth handlers rethrow it instead of a generic provider failure.

The SDKs use operating-system or Web Crypto randomness for keys, nonces, and session material. Never substitute deterministic or convenience randomness in production paths.

Transport V2 session recovery

Managed SDK requests can resend the original logical operation once after a fresh, verified attestation handshake, only on outer HTTP 400 with exactly x-opensecret-error-contract: 1 and x-opensecret-error-code equal to session_not_found or request_decryption_failed. The backend marks only an actually missing/expired session or failed AEAD authentication of the incoming request, before application dispatch. The resend uses a new session and request ID and applies to mutations and inference as well as reads.

These outer hints are unauthenticated. As with V1's best-effort recovery, an intermediary can forge one after the original operation executed and cause a duplicate under the new session. Per-session replay protection does not guarantee cross-session at-most-once execution; use application-level idempotency when needed.

Response decryption/framing failures, network failures, timeouts, partial streams, redirects, generic 400/503, and ordinary application errors do not trigger automatic resend. Prepared native-handoff redemption and session-bound OAuth callbacks require restarting their flows. There is no V1 fallback, and credentials remain inside encrypted requests. Token refresh remains proactive and coalesced; an authenticated expired-access response may refresh credentials for future operations without resending the failed operation.

TypeScript/React SDK

Install the published version selected for your application and backend. The 4.0.0 pin below illustrates the command; it is not a current-version claim:

bun add --exact @mapleai/sdk@4.0.0

Wrap the application with OpenSecretProvider and supply the backend URL and project/client ID from the environment you selected. The SDK requires both values; it does not select a backend for you.

Environment Backend configuration Authentication for validation
Local Actual HTTP loopback URL and port, with the project ID registered in that backend Local email/password or guest fixtures; ordinary local validation does not require provider OAuth
Hosted development HTTPS development backend, its project ID, and pcrConfig.environment = "development" A valid account in that development backend
Production HTTPS production backend, its project ID, and production PCR policy Intentionally selected production account and flow

The client's API URL, PCR policy, browser OAuth origin, and callback scheme are separate settings. Pointing the API at loopback does not change a consuming app's hosted OAuth origin. Select a compatible auth flow in that app before validation. Keep secrets out of public frontend build configuration.

import { OpenSecretProvider } from "@mapleai/sdk";
import type { ReactNode } from "react";

export function AppProviders({
  children,
  apiUrl,
  clientId,
  pcrEnvironment
}: {
  children: ReactNode;
  apiUrl: string;
  clientId: string;
  pcrEnvironment: "production" | "development";
}) {
  return (
    <OpenSecretProvider
      apiUrl={apiUrl}
      clientId={clientId}
      pcrConfig={{ environment: pcrEnvironment }}
    >
      {children}
    </OpenSecretProvider>
  );
}

Use useOpenSecret for authentication, encrypted application APIs, conversations, inference, and account operations. Internal developer tooling uses OpenSecretDeveloper and useOpenSecretDeveloper; preserve that surface when changing the public exports.

OAuth callback selection (4.1.0)

The three browser initiation methods accept an optional final callback URL:

const os = useOpenSecret();
await os.initiateGoogleAuth(inviteCode); // Existing provider default.
await os.initiateGoogleAuth(inviteCode, `${window.location.origin}/auth/google/callback`);
await os.initiateGitHubAuth(inviteCode, `${window.location.origin}/auth/github/callback`);
await os.initiateAppleAuth(inviteCode, `${window.location.origin}/auth/apple/callback`);

The SDK forwards a supplied URL unchanged as redirect_url; an omitted argument keeps the existing request shape. The backend validates exact membership in that project's provider settings. Its platform settings types also expose additional_redirect_urls?: string[] | null: when the provider object is supplied, omission or null preserves the list and [] clears it. See the backend contract.

Deploy backend callback-selection support and register the URL with the provider before selecting a non-default callback. An older backend ignores the new field and uses its default; the SDK does not silently retry with a different callback. Native Apple sign-in and the Rust SDK are unchanged.

System One decisions

systemOne answers typed questions (noul, choice, score) about a JSON state with probabilities instead of text (POST /v1/systemone). model is glm-5-3-flash by default (Continuum, calibrated, takes images) or one of Tinfoil's decision-small, decision-medium and decision-large (text only, up to 26 options per choice). It uses the signed-in session, or an API key through options.apiKey; useOpenSecret().systemOne is the same function:

import { systemOne, SystemOneError } from "@mapleai/sdk";

try {
  const { answers } = await systemOne({
    state: { ticket: "Charged twice, need a refund today." },
    questions: {
      is_urgent: { type: "noul", instructions: "Does the customer need help today?" },
      intent: {
        type: "choice",
        instructions: "What does the customer want?",
        criteria: { refund: "money back", question: "information", praise: "thanks" }
      },
      frustration: {
        type: "score",
        instructions: "How frustrated is the customer?",
        criteria: ["Low", "Medium", "High"]
      }
    }
  });
  if (answers.intent.type === "choice") {
    console.log(answers.intent.choice, answers.intent.confidence);
  }
} catch (error) {
  if (error instanceof SystemOneError) console.log(error.status, error.code); // 422, system_one_*
}

Choice options are numbered in the order of the criteria keys (JavaScript keeps insertion order for non-integer keys). Optional request fields are images (data: URLs shown with the state) and temperature (1 returns raw probabilities). Rejections carry the backend's status (422 for schema problems, 413 for size limits, chat's statuses for quota and capacity) and its x-opensecret-error-code.

Development

Use the pinned Nix shell and Bun version. bun.lock is the supported dependency lockfile; repository installation and updates use Bun. npm is used only to publish the built tarball, so do not create an npm lockfile.

nix develop --no-update-lock-file
bun install --frozen-lockfile --ignore-scripts
bun run format:check
bun run build

For tests without a configured backend, use the credential-free selection in build-sdk-publish.sh, which CI runs as the sdk-ts lane (just lane sdk-ts from the repository root). An unfiltered bun test also collects integration tests and requires the fixtures described below.

Integration tests read the variables documented in .env.example. The monorepo os-sdk-e2e lane (opensecret-sdk-e2e.sh) migrates disposable PostgreSQL, starts the in-tree services/opensecret/ backend from the same checkout on loopback, and creates disposable SDK fixtures. It does not depend on the hosted development service or stored test-account credentials.

Tests that spend model/provider capacity are opt-in with RUN_LIVE_AI=1 and are not part of the deterministic pull-request gate. Backend contract changes and SDK changes are validated together against that checkout; released clients and independently deployed backend versions still need compatibility review. Both SDKs accept local roots first, then fetch their selected environment's signed PCR history on a miss from MaplePrivacyLabs/Maple/master/services/opensecret/: pcrProdHistory.json for production and pcrDevHistory.json for development. The existing verification key, custom history URL overrides, and redirect rejection are unchanged. Embedded roots are versioned with each shipped SDK: editing or pruning in-tree roots does not replace the roots in published packages. Older published SDKs and installed clients still use the legacy OpenSecretCloud/opensecret URLs, which remain a manual compatibility mirror. Changing source defaults does not update those clients or publish an SDK. See the backend compatibility procedure.

Inspect the publishable npm artifact with:

bun run pack

Only dist/ is included in the package.

Publishing runs in GitHub Actions. To dispatch validation from sdk/, replace VERSION with the exact TypeScript version committed in package.json on master:

just publish-npm VERSION

This defaults to a dry run. See the SDK publishing guide for the protected publish action and the one-time registry setup.

Rust SDK

Add the selected published SDK version to a Rust application. This example illustrates an exact 4.0.0 pin rather than declaring the current version:

[dependencies]
maple-sdk = "=4.0.0"

Import the primary entry point with use maple_sdk::OpenSecretClient. See rust/README.md for native client examples and transport details.

Run the Rust validation from the sdk/ directory:

nix develop --no-update-lock-file -c bash -lc '
  set -euo pipefail
  cd rust
  cargo fmt --all -- --check
  cargo clippy --locked --all-targets --all-features -- -D warnings
  cargo test --locked --all-features --lib
  cargo doc --locked --no-deps --all-features
'

These library checks do not require a backend or account credentials. Integration tests use explicit backend fixtures and are separate from this default validation path; see Rust testing and opensecret-sdk-e2e.sh.

To dispatch validation from sdk/, replace VERSION with the exact Rust version committed in rust/Cargo.toml on master:

just publish-cargo VERSION

This defaults to a dry run. Both recipes only dispatch GitHub Actions; they do not build or publish packages locally. Each SDK has its own workflow and version, independent of Maple application releases. Follow the SDK publishing guide to publish.

Change discipline

  • Keep the TypeScript and Rust attestation policies aligned intentionally; neither SDK's passing tests prove parity with the other.
  • Treat API compatibility, authentication state, encrypted retry behavior, and PCR policy changes as security-sensitive.
  • Update source comments and focused tests with behavior changes instead of regenerating a standalone documentation site.
  • Validate the built npm package and Rust crate boundary before publishing a release.

License

MIT