Security
Credential boundaries, tenant authorization, webhook verification, network egress, and redaction rules for Social SDK applications.
Social SDK is a server-side library with explicit security boundaries. This page collects the rules that every integration shares; the linked guides cover each mechanism in depth.
Credentials stay on the server
Platform access tokens, managed-provider API keys, OAuth client secrets, refresh tokens, upload session URIs, and webhook secrets are server secrets. They must never reach a browser bundle, a client component, a public reference, a log line, or a fixture. A browser may send an authenticated request naming content and an application-owned account selection; it must never receive backend credentials or an unrestricted connected-account reference.
Store credentials durably and encrypted
MemoryCredentialStore, MemoryConnectionStore, and the other memory stores keep plaintext values in process memory and lose them on exit. Use them only in tests. The server package includes injected SQL and Redis stores and createEncryptedCredentialStore for AES-256-GCM credential encryption. Key records by tenant, principal, backend, platform, and account, and enforce grants before reading them. Encrypt connection records too: they can hold PKCE verifiers and private provider state.
Token refresh runs when your application requests it. Configure a refresh hook on CredentialManager and call resolve to check expiry, or call refreshOAuthToken (or refreshBlueskyOAuthSession) explicitly. Use a shared refresh lock and credential revisions together. Compare-and-set alone cannot prevent two provider requests with the same rotating refresh token. Keep credentials whose access tokens have expired so the refresh token remains available. See connect accounts.
Two separate authorization questions
Application authentication answers who the caller is. Social-account authorization answers whether that caller’s tenant may use a specific connected account. The SDK enforces the second question only when createSocial has an AuthorizationPolicy. The authorization call option carries the authenticated tenant and principal to that policy; on its own it grants nothing, and without a policy the client performs no access check. Never take an account ID from a request body without a server-side ownership check. See authorization.
No implicit network activity
Importing the SDK performs no network request, telemetry, timer, queue, or background poll. Every external call is one your application makes explicitly, with a bounded retry budget and cancellation.
Outbound URLs and private networks
URL checks in the SDK are static. They reject non-HTTPS schemes, localhost, credentials in URLs, and IP-literal hosts in private and reserved ranges. They do not resolve DNS, so a public hostname that resolves to a private address still passes.
Where media URLs go depends on the route:
- Managed backends and the Instagram, Threads, and TikTok adapters forward an
https-urlto the provider, which fetches it. Your server does not. - The Bluesky adapter fetches remote images itself. It takes an
allowMediaHostoption; restrict it to the hosts you serve media from. - Bluesky OAuth resolves handles, DID documents, and server metadata from URLs other parties control. Every OAuth network request requires both
assertEgressAllowedand an injectedfetchthat pins approved resolved addresses, including connection, refresh, and resource requests. The policy must reject private resolutions; ordinary fetch paired with a policy hook is insufficient. See the Bluesky transport contract.
Webhooks are verified before parsing
Each verifier checks the raw request bytes and throws unauthorized before any parsing. The mechanisms differ, and the returned VerifiedWebhook.bodyAuthenticated field says which kind you got:
- Zernio and every direct platform verifier compute an HMAC over the body, so the body is authenticated (
bodyAuthenticated: true). - Post for Me sends a shared secret in the
Post-For-Me-Webhook-Secretheader. The verifier compares that header only; the body is not authenticated (bodyAuthenticated: false).
Accepted events resolve every referenced account to authorized tenants; events with no common authorized tenant are quarantined rather than delivered. Payload tenant fields are never trusted. See process webhooks, including the direct platform verifier table.
Errors are redacted
Structured errors never contain credentials, tokens, signatures, media bytes, message content, or private account data. Diagnostics are read-only by default and follow the same redaction rules. For support and reconciliation, log correlationId, upstreamStatus, upstreamCode, and retryDisposition from SocialError instead of raw responses. SocialError does not expose provider request IDs. See the errors reference.
Native access is explicitly unsafe
Typed native modules and social.native(backend, { acknowledgeUnsafe: true }) bypass the client’s authorization policy and concurrency middleware. Applications must wrap them in their own authorization checks. See native access.
Uncertain writes are never retried blindly
A lost response after a public write is an unknown outcome, not a failure. Retrying it without reconciliation can double-post. Reconcile where the route supports status reads, and use a new idempotency key only after you know the write did not happen. See idempotency.