<>ts-stack
Get StartedArchitecturePackagesSpecsGuides
⌘K
Reference
Home
Get StartedOverviewInstallChoose your stackKey concepts
ArchitectureOverviewStack layersBEEF (BRC-62)BRC-100 Wallet InterfaceIdentity & AuthConformance pipeline
PackagesOverviewSDKWalletNetworkOverlaysMessagingMiddlewareHelpers
InfrastructureOverviewmessage-box-serveroverlay-serveruhrp-server-basicuhrp-server-cloud-bucketwabwallet-infrachaintracks-server
SpecsOverviewBRC-100 Wallet InterfaceOverlay HTTPMessage-box HTTPAuthsocket (WebSocket)BRC-31 Auth HandshakeBRC-29 Peer PaymentBRC-121 / HTTP 402ARC BroadcastMerkle ServiceStorage AdapterGASP SyncUHRPAir-Gap Optical (BRC-141)
ConformanceOverviewVector catalogTS runnerContributing vectors
GuidesOverviewBuild a wallet-aware appRun an overlay nodePeer-to-peer messagingHTTP 402 payments
ReferenceOverviewBRC indexRepository health
AboutVersioningContributingDoc agentDocumentation sources
Loading…
Edit this page on GitHub
© 2026 BSV Blockchain. ts-stack is open-source.
GitHubContributingVersioning

Public Service Edge Security

This page is the security and resource-control inventory for the HTTP and WebSocket services maintained in this repository. It covers the deployed infrastructure entrypoints and the reusable OverlayExpress, StorageServer, AdminServer, and ChaintracksService server classes.

The services are public protocols. Web apps on previously unknown domains, wallet UIs, mobile clients, server processes, and command-line clients must all be able to connect. Browser reachability is therefore public by default, while authorization remains enforced by protocol authentication, payment policy, per-identity quotas, input validation, and endpoint ownership rules.

Shared edge contract

Every maintained service installs controls in this order:

  1. explicit reverse-proxy trust, where applicable;
  2. security response headers and removal of X-Powered-By;
  3. CORS policy;
  4. a per-process in-flight request ceiling;
  5. a pre-authentication IP rate limit on state-changing work;
  6. endpoint-specific, bounded body parsing;
  7. BRC-103 or administrative authentication;
  8. a post-authentication identity rate limit;
  9. payment middleware where the endpoint is monetized; and
  10. route validation and a stable, non-sensitive error envelope.

The shared implementation is authored at infra/wab/src/security/edgePolicy.ts and synchronized to every deployable context with:

sh
pnpm sync:service-edge-policy
pnpm health:check

CI fails if a synchronized copy drifts.

CORS modes

<PREFIX>_CORS_MODE accepts:

ModeBehavior
publicDefault. Browser origins, including opaque null origins from sandboxed/file/mobile-webview contexts, receive Access-Control-Allow-Origin: *. Cookie credentials are never enabled.
allowlistOnly exact origins in <PREFIX>_CORS_ALLOWED_ORIGINS are accepted. Responses echo that origin and set Vary: Origin.
disabledRequests without Origin continue; cross-origin browser requests receive 403 ERR_ORIGIN_NOT_ALLOWED.

Setting an origin list without a mode selects allowlist for compatibility. Wildcard, opaque null, credentialed, path-bearing, query-bearing, and fragment-bearing allowlist entries fail startup. A shared CORS_MODE and CORS_ALLOWED_ORIGINS may be used as fallbacks.

When <PREFIX>_CORS_ALLOWED_HEADERS is omitted, a preflight response contains the known BSV protocol defaults plus every well-formed header named by Access-Control-Request-Headers. This is deliberate forward compatibility: browser clients and observability layers may add request identifiers or new protocol headers before every service operator upgrades. CORS is not an authentication boundary, and each endpoint still validates and authorizes the request itself. Set <PREFIX>_CORS_ALLOWED_HEADERS to an explicit comma-separated list only when the operator wants a strict browser header allowlist. Service-specific values override application-provided defaults; wildcard * is rejected. <PREFIX>_CORS_EXPOSED_HEADERS remains an explicit comma-separated override for response headers readable by browser JavaScript.

Public CORS does not make a protected endpoint anonymous. It only lets browser JavaScript attempt the same signed or token-authenticated protocol call that a mobile or server client can attempt. No service in this inventory uses ambient browser cookies as its authorization boundary.

governance/service-edge-browser-policy.json records this contract and every WAB, UHRP, Message Box, Chaintracks, OverlayExpress, and Wallet Storage integration. Repository health verifies the public wildcard/no-credentials default, additive well-formed request-header compatibility, strict operator header overrides, opaque-origin support, opt-in exact origin allowlists and credentials, disabled mode, service/global precedence, and the independence of CSP and CORS. The canonical tests are synchronized across the reusable Overlay and Storage implementations. Hosted container contracts probe both X-Correlation-ID and an otherwise unknown future header for every official service image.

CSP and other response headers

API responses use nosniff, deny framing, no-referrer, a restrictive permissions policy, an opener policy, a public-compatible Cross-Origin-Resource-Policy: cross-origin, and HSTS when Express has established a secure request through the configured proxy trust chain. Raw X-Forwarded-Proto is not trusted directly.

These headers do not replace protocol authentication, and CSP does not decide which browser origins may call an API. Deployments can override the defaults without patching source by setting the service-prefixed variables below. Deployment variables take precedence over programmatic defaults; omit them to use the library configuration. The value disabled omits headers that support omission.

VariableAccepted values
<PREFIX>_CONTENT_SECURITY_POLICYA single-line CSP, or disabled.
<PREFIX>_CROSS_ORIGIN_RESOURCE_POLICYcross-origin (default), same-site, same-origin, or disabled.
<PREFIX>_CROSS_ORIGIN_OPENER_POLICYsame-origin (default), same-origin-allow-popups, unsafe-none, or disabled.
<PREFIX>_FRAME_OPTIONSDENY (default), SAMEORIGIN, or disabled.
<PREFIX>_PERMISSIONS_POLICYA single-line Permissions Policy, or disabled.
<PREFIX>_STRICT_TRANSPORT_SECURITYtrue (default) or false; applies only to a trusted secure request.

HTML surfaces use an explicit CSP:

  • OverlayExpress exposes CSP and related header overrides through configureEdgePolicy({ securityHeaders }).
  • Wallet StorageServer exposes securityHeaders; AdminServer exposes adminSecurityHeaders.
  • API-only services retain the strict default-src 'none' baseline unless an operator deliberately overrides it.

Operators should add only the script, style, image, font, and connection origins actually required by their deployed UI. Wallet/pop-up integrations that need to retain an opener can select same-origin-allow-popups; embeddable operator UIs can coordinate CSP frame-ancestors with FRAME_OPTIONS.

Resource and error behavior

  • Body and timeout environment values are positive, bounded integers; invalid values fail startup.
  • Oversized bodies return 413 ERR_BODY_TOO_LARGE.
  • Malformed JSON returns 400 ERR_INVALID_BODY.
  • Local concurrency exhaustion returns 503 ERR_SERVER_BUSY and Retry-After: 1.
  • Rate exhaustion returns 429 ERR_RATE_LIMITED and standard draft-8 rate headers.
  • Internal exceptions are logged server-side and use stable external errors.
  • Request logs contain route and size metadata, not auth/payment headers, presentation keys, OTP stores, Shamir shares, private keys, or message bodies.

Per-process counters are not a global distributed quota. Replicated deployments must use a shared rate-limit/session store or enforce the aggregate limit at a trusted ingress. Proxy hop counts must match the actual deployment; permissive proxy trust is not supported.

Endpoint inventory

“Public” below means no application identity is required. All routes still receive the shared CORS, concurrency, timeout, and parser protections.

WAB

Prefix: WAB. JSON limit: 256 KiB. Concurrency: 200.

EndpointsAccess and controls
GET /infoPublic capability metadata. DevConsole is advertised only when explicitly enabled in a development or test runtime.
POST /auth/start, /auth/completePublic OTP exchange; 10 attempts/15 minutes per IP. Twilio provider failures and internal exceptions are not exposed.
POST /account/delete/start, /account/delete/completeOTP-confirmed deletion; 5 attempts/15 minutes.
POST /user/linkedMethods, /user/unlinkMethod, /user/deletePresentation-key operations; 120 operations/15 minutes.
POST /faucet/requestPresentation-key faucet workflow; 5 attempts/hour.
POST /share/store, /share/retrieve, /share/update, /share/deleteOTP-confirmed Shamir-share operations; 10 attempts/15 minutes plus persistent user/IP operation limits.

The DevConsole method requires both DEV_CONSOLE_AUTH_METHOD_ENABLED=true and NODE_ENV=development|test. Its singleton OTP store is shared across the auth and share controllers. It logs the development OTP by design, but never logs presentation keys, request payloads, or the OTP store. Production and staging cannot enable it.

UHRP basic server

Prefix: UHRP. JSON limit: 256 KiB. Upload limit: 64 MiB. Concurrency: 100. Pre-auth: 300/minute per IP. Authenticated: 1,000/minute per identity.

EndpointsAccess and controls
Static GET/HEAD under the public object directoryPublic immutable object retrieval with CDN MIME handling.
PUT /putHMAC-authorized streaming upload. Authorization and declared size are checked before body consumption; bytes stream to a private temporary file, hash incrementally, and atomically commit without overwriting an existing path.
POST /quotePublic, bounded pricing calculation.
POST /uploadBRC-103 identity required; payment price is based on declared size/retention. Returns an upload authorization rather than accepting object bytes.
GET /list, GET /findBRC-103 identity required; queries are scoped to the authenticated uploader.
POST /renewBRC-103 identity and payment policy; metadata ownership is checked.

The default 64 MiB ceiling remains configurable and bounded, but upload memory use is independent of object size. Truncated, oversized, unauthorized, expired, path-invalid, and concurrent-overwrite attempts fail without publishing a partial object.

UHRP cloud-bucket server

Prefix: UHRP. JSON limit: 256 KiB. Concurrency: 200. Pre-auth: 300/minute per IP. Authenticated: 1,000/minute per identity.

EndpointsAccess and controls
Static GET/HEAD contentPublic object retrieval; cloud storage remains the object source of truth.
POST /advertiseAdministrative operation using an Authorization: Bearer token of at least 32 characters, compared in constant time.
POST /quotePublic, bounded pricing calculation.
POST /uploadBRC-103 identity required; creates the cloud upload workflow and applies payment policy.
GET /list, GET /findBRC-103 identity required and uploader-scoped.
POST /renewBRC-103 identity required; ownership and payment policy apply.

Production HTTPS enforcement relies only on Express req.secure after an explicit proxy trust configuration. Nginx and the service configuration cap JSON bodies at 256 KiB and use 60-second request/upstream timeouts.

Message Box

Prefixes: MESSAGE_BOX, MESSAGE_BOX_WEBSOCKET. JSON limit: 4 MiB. WebSocket message limit: 1 MiB. Concurrency: 200. Pre-auth: 300/minute per IP. Authenticated HTTP: 1,000/minute per identity.

EndpointsAccess and controls
GET /docs, GET /openapi.jsonPublic API documentation.
POST /sendMessageBRC-103 identity plus recipient permission/fee policy. At most 100 recipients, 128-byte box names, 256-byte message IDs, and 1 MiB message bodies.
POST /listMessages, /acknowledgeMessageBRC-103 identity; database queries/deletes are recipient-scoped.
POST /registerDevice, GET /devicesBRC-103 identity; device records are identity-scoped and returned push tokens are truncated.
GET /permissions/get, /permissions/list, /permissions/quote; POST /permissions/setBRC-103 identity; permission ownership and 100-recipient quote cap apply.
WebSocket authenticatedThe claimed key must match the identity discovered by the signed BRC-103 event. Payload claims are never trusted as identity.
WebSocket joinRoom, leaveRoomOnly {authenticatedIdentityKey}-{messageBox} names are accepted.
WebSocket sendMessageReuses the HTTP handler’s validation, permission, fee, duplicate, and persistence policy. Paid sends fall back to authenticated HTTP. Notifications are delivered only to sockets mapped to the recipient’s BRC-103 identity, never broadcast globally.

The nginx body limit is 4 MiB, with bounded header, body, send, and upstream timeouts. WebSocket origins use the same public/allowlist/disabled CORS mode as HTTP.

Chaintracks server and reusable ChaintracksService

Prefixes: CHAINTRACKS, CHAINTRACKS_CDN. JSON limit: 256 KiB. Concurrency: 200 API / 100 CDN.

All Chaintracks data is public. Mutation is limited to the compatibility POST /addHeaderHex path, which validates the submitted block header in the Chaintracks implementation.

SurfaceEndpoints
Service metadataGET /, GET /robots.txt
v1 JSONGET /getChain, /getInfo, /getPresentHeight, /findChainTipHashHex, /findChainTipHeaderHex, /findHeaderHexForHeight, /findHeaderHexForBlockHash, /getHeaders, /getFiatExchangeRates; POST /addHeaderHex
v2 JSON/binaryGET /v2/network, /v2/tip, /v2/header/height/:height, /v2/header/hash/:hash, /v2/headers, /v2/tip.bin, /v2/header/height/:height.bin, /v2/header/hash/:hash.bin, /v2/headers.bin
Optional bulk CDNStatic GET/HEAD under the configured bulk-header root

Responses use stable internal-error envelopes. Query/body limits and the connection policy bound malformed or slow clients.

OverlayExpress

Prefix: OVERLAY. JSON limit: 8 MiB. Binary limit: 64 MiB. Concurrency: 200.

SurfaceEndpoints and access
UI and healthPublic GET /, /health/live, /health/ready, /health.
Discovery/docsPublic GET /listTopicManagers, /listLookupServiceProviders, /getDocumentationForTopicManager, /getDocumentationForLookupServiceProvider.
Overlay protocolPublic POST /submit, /lookup; conditional /arc-ingest; GASP /requestSyncResponse, /requestForeignGASPNode; BASM /requestTopicAnchorTip, /requestTopicAnchorRange, /requestAdmittedList, /requestCompoundMerklePath, /requestRawTransactions.
Public admin metadataGET /admin/config returns only node name and the configured public admin identity key.
Protected admin readsGET /admin/stats, /admin/ship-records, /admin/slap-records, /admin/bans.
Protected admin writesPOST /admin/health-check, /admin/ban, /admin/unban, /admin/remove-token, /admin/syncAdvertisements, /admin/startGASPSync, /admin/startBASMSync, /admin/evictUnproven, /admin/refreshUnprovenProofs, /admin/maintainUnproven, /admin/evictOutpoint, /admin/janitor.

Admin operations require either the configured Bearer token or BRC-103 authentication matching the configured admin identity key. The maintained deployment rejects configured admin tokens shorter than 32 characters, and Bearer-token comparison is constant-time. Janitor outbound health checks reject credentials, queries/fragments, HTTP, nonstandard ports, localhost/private/special IP literals, and redirects by default. allowPrivateHosts is an explicit isolated-development override.

Verbose request logging redacts auth, cookie, token, payment, signature, and nonce headers and records body type/size only. Overlay UI CSP and edge limits are configurable through configureEdgePolicy.

Wallet StorageServer and AdminServer

Prefixes: WALLET_STORAGE, WALLET_ADMIN. Storage JSON limit: 30 MiB. Storage binary limit: 8 MiB. Storage concurrency: 200. Admin JSON limit: 1 MiB. Admin concurrency: 50.

SurfaceEndpoints and access
Storage metadataPublic GET /, GET /robots.txt.
Blob uploadBRC-103-authenticated PUT /action-batch/:batchId/blob/:digest; action-batch ownership and digest validation are enforced by storage.
Storage RPCBRC-103-authenticated POST /; optional payment middleware, method allowlist, authenticated AuthId checks, and JSON-RPC validation apply.
Admin liveness/UI assetsGET /healthz, /admin, /admin/assets/bsv-sdk.js. If admin identity configuration is absent, the server fails closed with 503 for later routes.
Admin APIAuthenticated and allowlisted identity required for stats, events, tasks, users, call history, UTXO review, transaction review/decode/rebroadcast, and manual task execution.

Storage pre-auth limits default to 300/minute per IP and authenticated limits to 1,000/minute per identity; callers can provide shared stores. Short-request and RPC logging records metadata only. Nginx caps requests at 30 MiB and does not replay state-changing RPC requests to another upstream.

Threat model and retest checklist

The boundary assumes TLS is terminated either in-process or at the explicitly trusted ingress. Databases, bucket credentials, server private keys, admin tokens, and shared rate-limit/session stores are operator-managed secrets.

Review each release against these abuse cases:

  • anonymous body/connection exhaustion before authentication;
  • authenticated high-cardinality or expensive requests;
  • spoofed forwarding headers and identity claims;
  • cross-origin browser compatibility and allowlist enforcement;
  • parser/streaming size differentials between proxy and application;
  • path traversal, overwrite races, and object ownership;
  • message recipient/room confusion and global broadcast leakage;
  • SSRF through overlay health checks and redirects;
  • replay of state-changing requests by proxies;
  • sensitive headers, bodies, OTPs, shares, keys, or provider errors in logs;
  • verbose internal errors returned to public clients; and
  • horizontal replicas bypassing per-process limits.

Required local evidence for edge-policy changes:

sh
pnpm health:check
pnpm build
pnpm --filter @bsv/overlay-express test
pnpm --filter @bsv/authsocket test
pnpm --filter @bsv/wallet-toolbox test

Each affected standalone infra directory must also pass its own build, lint, tests, and npm audit. The pull request must then pass repository CI, conformance, CodeQL, SonarCloud analysis, and patch coverage before merge.