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

@the-situation/privy-backend

v0.5.1

Published

Effect-native Privy backend services for The Situation stack

Downloads

40

Readme

@the-situation/privy-backend

Effect-native Privy backend for auth, wallet management, and Starknet-ready account flows used by The Situation webapp.

Local Development

cp packages/privy-backend/.env.example packages/privy-backend/.env
# Fill required env vars in .env

bun run --filter '@the-situation/privy-backend' dev

Health check:

curl http://localhost:3000/health

Server endpoints include:

  • GET /health
  • POST /privy/create-wallet
  • POST /privy/public-key
  • POST /privy/deploy-wallet
  • POST /privy/execute
  • GET /privy/user-wallets

Environment Variables

| Variable | Required | Default | Description | |---|---|---|---| | PRIVY_APP_ID | Yes | — | Privy app ID | | PRIVY_APP_SECRET | Yes | — | Privy app secret | | PRIVY_WALLET_AUTH_PRIVATE_KEY | No | — | Optional raw-sign authorization private key override | | RPC_URL | Yes | — | Starknet RPC URL | | READY_CLASS_HASH (or READY_CLASSHASH) | Yes | — | Ready account class hash | | CLIENT_URL | Recommended | — | Trusted frontend origin used in Privy wallet signing requests | | PRIVY_API_BASE_URL | No | https://api.privy.io | Privy API base URL | | HOST | No | 0.0.0.0 | HTTP bind host | | PORT | No | 3000 | HTTP bind port | | ALLOW_ORIGIN_OVERRIDE | No | false | Enables direct caller override of outbound Origin header | | ORIGIN_OVERRIDE_ALLOWLIST | Conditionally (required if ALLOW_ORIGIN_OVERRIDE=true) | — | Comma-separated absolute origins | | PAYMASTER_URL | No | https://sepolia.paymaster.avnu.fi (when enabled) | Paymaster RPC URL | | PAYMASTER_MODE | No | sponsored | sponsored or default | | PAYMASTER_API_KEY | Conditionally (required for sponsored mode) | — | Paymaster API key | | GAS_TOKEN_ADDRESS | Conditionally (required for default mode) | — | Gas token for default paymaster mode | | IDEMPOTENCY_STORE | No | memory | memory (default, process-local) or redis (shared across instances) | | REDIS_URL | Conditionally (required when IDEMPOTENCY_STORE=redis) | — | Redis connection URL used by the shared idempotency store | | IDEMPOTENCY_TTL_SECONDS | No | 300 | TTL (seconds) for cached idempotent responses | | AUTH_CACHE_FALLBACK_TTL_SECONDS | No | 60 | Fallback TTL for the JWT verify cache when verified claims have no exp. | | PAYMASTER_TOKENS_TTL_SECONDS | No | 300 | TTL for the service-scoped cache of paymaster.getSupportedTokens() results. | | AUTH_CACHE_DISABLED | No | false | Set to a truthy value (true/1/yes/on, case-insensitive) to bypass the JWT verify cache entirely (verify every request upstream). Incident-response lever — no code deploy required. | | PAYMASTER_TOKENS_CACHE_DISABLED | No | false | Set to a truthy value (true/1/yes/on, case-insensitive) to bypass the paymaster getSupportedTokens cache entirely (fetch on every execute). Incident-response lever — no code deploy required. |

Performance

The backend ships with three in-process caches that cut redundant upstream calls on hot request paths. All are process-local and bounded.

  • JWT verify cache (privyAuthPlugin). Verified Privy tokens are cached in a bounded (10k entries) in-memory map keyed by the sha256 digest of the raw token. TTL is taken from the JWT exp claim when present, else falls back to AUTH_CACHE_FALLBACK_TTL_SECONDS (default 60s). Invalid tokens and upstream outages are never cached. Concurrent requests with the same bearer token dedupe to a single upstream verify.
  • Paymaster tokens cache (service-scoped). When PAYMASTER_MODE=default and no GAS_TOKEN_ADDRESS is configured, the service probes paymaster.getSupportedTokens() once per PAYMASTER_TOKENS_TTL_SECONDS (default 300s) instead of per-execute. Concurrent cold-cache executes dedupe to one fetch. Failures are never cached.
  • Memoized RpcProvider. Each makePrivyStarknetService instance creates one RpcProvider and reuses it for all waitForTransaction calls and createAccount constructions — sharing the underlying keep-alive connection pool.

Both TTL caches can be opted out by passing null to the authCache / paymasterTokensCache option on the relevant factory. Tests inject custom clocks and cache instances via the same options.

For ops, the same opt-out is exposed via env vars without a code deploy: set AUTH_CACHE_DISABLED=true to verify every JWT upstream, or PAYMASTER_TOKENS_CACHE_DISABLED=true to fetch paymaster tokens on every execute. Both accept the case-insensitive truthy strings true/1/yes/on; anything else (including unset, empty, or false) keeps the default cache enabled. Worked example: if Privy revokes a token mid-window, the cache may still serve the cached claims for up to AUTH_CACHE_FALLBACK_TTL_SECONDS (or until the JWT exp) — flipping AUTH_CACHE_DISABLED=true and rolling the deployment makes every request re-verify upstream until the incident is resolved.

Webapp Integration Contract

Auth semantics

  • Authorization: Bearer <privy_user_jwt> is required for all /privy/* endpoints.
  • 401 is returned for missing/invalid auth.
  • 503 is returned when upstream auth verification is unavailable.
  • Wallet-scoped endpoints enforce authenticated user ownership checks.

Idempotency semantics

  • POST /privy/deploy-wallet and POST /privy/execute accept optional Idempotency-Key header.
  • Reusing the same key with the same authenticated user and same request payload replays the previous result.
  • Reusing the same key with a different payload returns 409.
  • If omitted, service generates an idempotency key for outbound Privy signing calls.
  • The backend is pluggable via IDEMPOTENCY_STORE:
    • memory (default) — process-local in-memory dedupe. Multi-machine deployments MUST either route all write traffic to a single machine or switch to the shared backend before scaling horizontally.
    • redis — shared replay store backed by Redis. Safe across multiple instances. A Redis client must be wired into the package via resolveIdempotencyStore({ redisClientFactory }) — this package does not take a direct redis dependency.

Deploy wallet request

POST /privy/deploy-wallet

{
  "walletId": "wallet-123",
  "wait": true,
  "usePaymaster": false
}
  • wait maps to waitForReceipt.
  • usePaymaster enables paymaster-aware account execution path.

Execute request

POST /privy/execute

{
  "walletId": "wallet-123",
  "wait": true,
  "usePaymaster": true,
  "call": {
    "contractAddress": "0x...",
    "entrypoint": "transfer",
    "calldata": {
      "recipient": "0x...",
      "amount": "1"
    }
  }
}
  • Provide exactly one of call or calls.
  • Response shape is flat (walletId, address, publicKey, transactionHash, result, optional receipt).

Origin override hardening

  • Default mode (ALLOW_ORIGIN_OVERRIDE=false) ignores caller-provided origin and uses configured CLIENT_URL.
  • If override mode is enabled, caller origin must be:
    • a valid absolute http(s) URL, and
    • present in ORIGIN_OVERRIDE_ALLOWLIST.
  • CLIENT_URL is required when ALLOW_ORIGIN_OVERRIDE=true. It is the fail-closed fallback origin used when the caller does not supply one.
  • Configuration fails closed if override mode is enabled without a non-empty allowlist.
  • ORIGIN_OVERRIDE_ALLOWLIST entries must be exact origins — scheme + host + optional port only. The loader rejects:
    • any path component (including a bare trailing slash like https://ok.com/),
    • any query string (?foo=bar) or fragment (#frag),
    • any userinfo component (https://user:[email protected]URL.origin silently drops userinfo, so this is rejected to prevent credential-bearing env entries from collapsing to the same stored origin as a clean entry),
    • any wildcard / pattern tokens (*, ?, URL-encoded %2A/%2a),
    • schemes other than http or https,
    • malformed URLs.
  • These format checks run regardless of whether override is enabled — a misconfigured allowlist in a dormant ALLOW_ORIGIN_OVERRIDE=false environment still fails loud so the error is caught before the flag is flipped.
  • Case is normalized via URL.origin (HTTPS://Example.COM:443 canonicalizes to https://example.com).
  • Duplicate entries are deduped silently.

Startup warning

When ALLOW_ORIGIN_OVERRIDE=true resolves successfully, the loader emits exactly one structured warning per successful config load:

console.warn('[privy-starknet] origin-override-enabled', {
  marker: 'origin-override-enabled',
  allowlistSize: <N>,
});

Grep marker for log pipelines: [privy-starknet] origin-override-enabled. Pipe this to your observability tool to alert on services that boot with override enabled.

Audit log format

makePrivyStarknetService({ ..., auditLog }) accepts an optional synchronous sink that receives one OriginOverrideAuditEvent per rawSign call. If you do not pass an auditLog sink, audit events are dropped silently — wire the sink to your logging pipeline or origin-override decisions will be unobservable in production.

type OriginAuditRoute = 'rawSign';

interface OriginOverrideAuditEvent {
  readonly decision: 'allowed' | 'denied' | 'fallback' | 'no-override';
  readonly reason:
    | 'allowed'
    | 'not-in-allowlist'
    | 'invalid-url'
    | 'no-caller-origin'
    | 'override-disabled'
    | 'allowlist-empty';
  readonly route: OriginAuditRoute;  // closed union — currently only 'rawSign'
  readonly resolvedOrigin?: string;  // the outbound Origin header (when decided)
  readonly allowlistSize: number;
  readonly userIdDigest?: string;    // sha256(appId + ':' + userId), hex, truncated to 12 chars
}

Worked example (an allowed decision on a two-entry allowlist):

{
  "decision": "allowed",
  "reason": "allowed",
  "route": "rawSign",
  "resolvedOrigin": "https://ok.com",
  "allowlistSize": 2,
  "userIdDigest": "a1b2c3d4e5f6"
}

Decision taxonomy:

| Caller origin | allowOriginOverride | decision | reason | |---|---|---|---| | (any) | false | no-override | override-disabled | | empty | true | fallback | no-caller-origin | | malformed URL | true | denied | invalid-url | | not in allowlist | true | denied | not-in-allowlist | | in allowlist | true | allowed | allowed |

Notes for ops:

  • userIdDigest is a truncated salted sha256 of the caller-supplied userId, not the plaintext userId. The salt is the Privy appId, so ops correlate by computing sha256(appId + ':' + userId).slice(0, 12) client-side. Within a single tenant the digest is stable; across tenants the same userId maps to distinct digests (defeats cross-deployment rainbow correlation). The sink never sees the raw userId, messageHash, or userJwt.
  • One audit event is emitted per rawSign invocation. buildReadyAccount does not double-emit — only the inner rawSign surface is audited.
  • The OriginOverrideAuditSink signature is declared synchronous ((event) => void) to nudge callers toward sync sinks. At runtime the impl tolerates async sinks: both synchronous throws and rejected promises are swallowed so a broken logger never aborts a sign call.
  • Operators must wire this sink into their logging pipeline. Without a sink, decisions are silent — you lose the audit trail entirely. Ensure the sink is non-blocking (pipe to your observability tool asynchronously so it does not stall signing).

Deploying To Fly.io

This package includes:

  • packages/privy-backend/fly.toml
  • Dockerfile.privy-backend (repo root, indexer-style workspace build)

The Docker build uses the workspace bun.lock with --frozen-lockfile for reproducible installs.

cd packages/privy-backend

# First-time setup
fly apps create situation-privy-backend

# Required only when using the default in-memory idempotency store
# (IDEMPOTENCY_STORE unset or =memory). When IDEMPOTENCY_STORE=redis is
# configured with a REDIS_URL and client factory, replay state is shared
# across machines and you can scale horizontally.
fly scale count 1

# Set required secrets
fly secrets set \
  PRIVY_APP_ID=your-app-id \
  PRIVY_APP_SECRET=your-app-secret \
  RPC_URL=https://api.cartridge.gg/x/starknet/sepolia \
  READY_CLASS_HASH=0xready-account-class-hash \
  CLIENT_URL=https://your-webapp.example

# Optional: enable tightly allowlisted origin override
# fly secrets set \
#   ALLOW_ORIGIN_OVERRIDE=true \
#   ORIGIN_OVERRIDE_ALLOWLIST=https://app.the-situation.com,https://staging.the-situation.com

# Optional secrets
# fly secrets set PAYMASTER_API_KEY=your-paymaster-key

fly deploy

Validate deployment:

curl https://situation-privy-backend.fly.dev/health

Useful Fly commands:

fly logs
fly machines list
fly machines restart