@sineforge/sdk
v3.0.0
Published
Official JavaScript/TypeScript client for Sineforge Backend-as-a-Service platform
Maintainers
Readme
Sineforge JavaScript SDK
Official JavaScript and TypeScript client for Sineforge.
Version 2 uses one flow-based authentication model across browser, native, desktop, CLI, and server runtimes. Browser users keep an HttpOnly session cookie; non-browser clients keep an opaque session token in memory. The SDK does not decode, refresh, or persist end-user credentials.
Install
npm install @sineforge/sdkFor development, a Git commit can also be pinned directly. The package prepare script builds
the distributable exports during installation:
{
"dependencies": {
"@sineforge/sdk": "github:fei-bai-inc/sineforge-sdk-js#<commit>"
}
}Node.js 18 or later is required. OAuth APIs require the Web Crypto global API.
On Node.js 18, start the application with node --experimental-global-webcrypto app.mjs;
Node.js 20 and later expose it by default. Browsers require a secure context or localhost.
Create a client
import { createClient } from '@sineforge/sdk';
const sineforge = createClient({
baseUrl: 'https://api.example.com',
anonKey: 'optional-public-project-key',
});baseUrl may include a gateway prefix such as https://example.com/sineforge. All SDK endpoints,
authentication form actions, redirects, and the Realtime socket path retain that prefix.
Use createAdminClient() only in trusted server code:
import { createAdminClient } from '@sineforge/sdk';
const admin = createAdminClient({
baseUrl: process.env.SINEFORGE_URL!,
apiKey: process.env.SINEFORGE_ADMIN_API_KEY!,
});Admin/API-key bearer credentials and end-user sessions are separate credential classes. Never pass an admin key to browser code.
Authentication
Sineforge publishes the available flow endpoints at runtime. Auth methods and the first native session request discover and cache that contract automatically. You can also load it explicitly:
const { data: config, error } = await sineforge.auth.getConfig();The application must render the returned ui.nodes dynamically. Fields, validation messages,
password rules, verification steps, and enabled external providers can change by project.
Browser apps
To use the deployment's hosted authentication UI, start a browser flow and navigate to its URL:
const { data, error } = await sineforge.auth.startBrowserFlow('login', {
returnTo: new URL('/account', window.location.origin).toString(),
});
if (data) window.location.assign(data.url);returnTo can also be a native app destination such as myapp://auth/callback. Configure the
destination in the project's allowed return URLs. The SDK preserves application return URLs,
including their origin and path; the server enforces the allowlist.
On the configured login page, read the flow query parameter and load its current UI:
const flowId = new URLSearchParams(window.location.search).get('flow');
if (!flowId) throw new Error('Missing flow');
const { data: flow } = await sineforge.auth.getBrowserFlow('login', flowId);Render flow.ui.nodes into a native HTML form:
<form action={flow.ui.action} method={flow.ui.method}>
{/* Render every flow.ui.nodes entry, including hidden CSRF fields. */}
</form>A native form uses Kratos's configured UI for subsequent steps and errors. returnTo controls
where a completed flow returns; it does not choose the page that renders the flow.
For an application-owned UI, use the browser JSON methods instead:
const created = await sineforge.auth.createBrowserFlow('settings', {
returnTo: new URL('/account', window.location.origin).toString(),
});
// Render created.data.ui.nodes. Inside the form's submit handler:
event.preventDefault();
const fields = Object.fromEntries(new FormData(form, event.submitter)) as Record<string, string>;
const result = await sineforge.auth.submitBrowserFlow(created.data!, fields);Preserve hidden CSRF fields and the clicked submit button, including method or provider.
These methods use browser cookies and Accept: application/json; form fields remain URL-encoded
so dotted trait names retain HTML form semantics. They never adopt native session tokens.
Render returned flows, including validation messages and rotated settings IDs. Handle
session_refresh_required, session_aal2_required and session_inactive in your own login UI,
with a fixed return to account settings. Clear and re-enter sensitive fields after reauthentication.
Handle redirectBrowserTo and continueWith explicitly: provider authorization needs browser
navigation, while settings, verification and recovery continuations can use the app's own routes.
Keep destinations constrained to the app, discovered auth endpoints or a server-issued provider
redirect. External-provider callbacks and emailed links still use the deployment's configured UI.
Do not pass a browser flow to submitFlow(), which is reserved for native API flows. Neither
browser method stores passwords or form values. Self-service request and response bodies are
omitted from SDK debug logs.
External sign-in buttons are ordinary nodes (typically group === "oidc" and
attributes.name === "provider"). Never hardcode a provider list; render what the flow publishes.
Read the projected Sineforge session after the flow returns:
const { data: session, error } = await sineforge.auth.getCurrentSession();
// An anonymous browser returns { data: null, error: null }.Browser sign-out is also a navigation flow. Pass the application's return destination, which must be in the deployment's return allowlist. Omitting it uses the deployment's default logout destination. Navigate to the one-time URL to actually revoke the session and clear the cookie:
const { data } = await sineforge.auth.signOutBrowser({
returnTo: new URL('/auth/sign-in', window.location.origin).toString(),
});
if (data) window.location.assign(data.url);Supported browser flow kinds are login, registration, recovery, verification, and
settings.
Native, desktop, CLI, and trusted server runtimes
Create an API flow, render or otherwise collect the fields described by ui.nodes, and submit the
current flow object:
const created = await sineforge.auth.createApiFlow('login');
if (!created.data) throw created.error;
const completed = await sineforge.auth.submitFlow(created.data, {
identifier: '[email protected]',
password: 'user-entered-password',
method: 'password',
});
if (completed.data && 'sessionToken' in completed.data) {
// submitFlow() has already stored this opaque token in this client instance.
console.log(completed.data.session.id);
}A validation response is returned as an updated AuthFlow with field messages, not as a transport
error. Registration can be multi-step (for example, profile selection followed by credentials), so
continue from the returned flow instead of assuming one fixed form.
You can restore an opaque token into a new request-scoped client:
const sineforge = createClient({
baseUrl: process.env.SINEFORGE_URL!,
sessionToken: encryptedSessionStore.read(),
});
// Equivalent after construction:
sineforge.auth.setSessionToken(token);The SDK sends the token in the runtime-advertised Sineforge session header for Sineforge APIs. It uses Bearer form only for direct identity endpoints that require it. It never sends the anon bearer alongside a native session.
Restored tokens work on the first database, storage, AI, or function request without a preceding
auth.getConfig() call. Concurrent requests share discovery; if it fails, no protected request is
sent and the next request can retry discovery.
await sineforge.auth.getCurrentSession();
await sineforge.auth.getWhoami();
await sineforge.auth.signOutNative(); // revokes and clears the tokenProfiles
Application profile data is separate from identity credentials:
const profile = await sineforge.auth.getProfile(userId); // public projection
const updated = await sineforge.auth.updateProfile({ name: 'Ada' }); // current sessionChange email, password, and other identity credentials through the settings flow rather than the
profile API.
Session state events
const unsubscribe = sineforge.auth.onAuthStateChange((event) => {
// signedIn | signedOut | credentialChanged
});Realtime reconnects when the end-user or bearer credential boundary changes.
SSR and Next.js
Import request-scoped helpers from @sineforge/sdk/ssr.
Client Components use the browser's HttpOnly cookie:
import { createBrowserClient } from '@sineforge/sdk/ssr';
const sineforge = createBrowserClient({
baseUrl: process.env.NEXT_PUBLIC_SINEFORGE_URL!,
anonKey: process.env.NEXT_PUBLIC_SINEFORGE_ANON_KEY,
});Server Components can forward the session cookie from the incoming request:
import { cookies } from 'next/headers';
import { createServerClient } from '@sineforge/sdk/ssr';
const sineforge = createServerClient({
baseUrl: process.env.NEXT_PUBLIC_SINEFORGE_URL!,
cookies: await cookies(),
});The cookie reader forwards only the configured session cookie and is appropriate for normal API calls. A server handler that loads or proxies a browser self-service flow must pass the complete incoming Cookie header so temporary CSRF/flow cookies are retained:
const sineforge = createServerClient({
baseUrl: process.env.NEXT_PUBLIC_SINEFORGE_URL!,
cookieHeader: request.headers.get('cookie') ?? undefined,
});For a native token, pass sessionToken instead. Supplying both browser cookies and a native token
is rejected. The SSR helpers do not refresh tokens, mint cookies, copy identity cookies to another
domain, or provide auth mutation wrappers.
Database
const { data, error } = await sineforge.database
.from('posts')
.select('id,title,created_at')
.eq('published', true)
.order('created_at', { ascending: false });
await sineforge.database.from('posts').insert({ title: 'Hello' });
await sineforge.database.from('posts').update({ title: 'Updated' }).eq('id', postId);
await sineforge.database.from('posts').delete().eq('id', postId);
await sineforge.database.rpc('publish_post', { post_id: postId });Use database.schema('analytics') or db: { schema: 'analytics' } to select a Postgres schema.
Storage
const avatars = sineforge.storage.from('avatars');
await avatars.upload('user/avatar.png', file);
const { data } = await avatars.download('user/avatar.png');
await avatars.list('user');
await avatars.remove(['user/avatar.png']);Functions
const { data, error } = await sineforge.functions.invoke('send-welcome', {
body: { userId },
});Realtime
await sineforge.realtime.connect();
const subscribed = await sineforge.realtime.subscribe('room:general');
const off = sineforge.realtime.on('message', (message) => {
console.log(message);
});
off();
sineforge.realtime.unsubscribe('room:general');
sineforge.realtime.disconnect();Native clients handshake with { sessionToken }, admin/anonymous clients with { token }, and
browser clients with an empty auth payload plus their cookie.
AI, email, and payments
await sineforge.ai.chat.completions.create({
model: 'openai/gpt-4o-mini',
messages: [{ role: 'user', content: 'Hello' }],
});
await sineforge.emails.send({
to: '[email protected]',
subject: 'Welcome',
html: '<p>Hello</p>',
});
await sineforge.payments.stripe.createCheckoutSession({
priceId: 'price_123',
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
});Errors and debug logs
Most high-level modules return { data, error }. Transport failures use SineforgeError:
if (error) {
console.error(error.error, error.statusCode, error.message);
}debug: true logs request diagnostics. Authorization, cookies, runtime session headers, passwords,
nested tokens, flow IDs, verification codes, and one-time logout URLs are redacted.
Development
npm install
npm test -- --run
npm run typecheck
npm run lint
npm run build
npm run test:package
npm run test:git-installLicense
Apache-2.0
OAuth / OpenID Connect sign-in
Use the standalone public client for Sineforge's optional Hydra issuer. Register an exact HTTPS callback (loopback HTTP is allowed for development) in Console → Authentication → OAuth applications, and enable the server. The issuer and callback must not contain a query or fragment; the issuer uses a dedicated origin.
import { createOAuthClient, type OAuthTransaction } from '@sineforge/sdk';
const oauth = createOAuthClient({
issuer: 'https://identity.example.com/',
clientId: 'registered-public-client-id',
redirectUri: 'https://app.example.com/oauth/callback',
});
// Before redirect: preserve only the short-lived state/nonce/PKCE transaction.
const { url, transaction } = await oauth.createAuthorizationRequest([
'openid',
'profile',
'email',
'offline_access',
]);
sessionStorage.setItem('oauth-transaction', JSON.stringify(transaction));
window.location.assign(url);
// In the callback: consume once, including on failure. Coalesce duplicate React
// effects so a one-use authorization code is never exchanged twice.
const callback = window.location.href;
const saved = sessionStorage.getItem('oauth-transaction');
sessionStorage.removeItem('oauth-transaction');
window.history.replaceState(null, '', '/oauth/callback');
if (!saved) throw new Error('Start a new sign-in');
const session = await oauth.completeAuthorization(callback, JSON.parse(saved) as OAuthTransaction);
const profile = await oauth.getUserInfo(session);
const renewed = await oauth.refresh(session); // requires granted offline_access
await oauth.revoke(renewed);completeAuthorization checks transaction age and binding, callback, state, nonce,
issuer, audience, expiry, RS256 signature and JWKS. getUserInfo requires the same
subject as the verified ID token. Refresh checks the identity again and returns
rotated credentials; replace the old session with its result. Errors reject with
OAuthError, whose code identifies protocol errors without retaining secret
response bodies. All network operations have a 15-second timeout and omit cookies.
Keep OAuthSession and its tokens in memory or appropriate server-side storage.
The SDK does not persist tokens, attach them to the normal API client, or create
Kratos sessions. With explicit instance and application opt-in, user access tokens can also authorize Database, Storage and Realtime through a resource token provider. Storage and Realtime accept their corresponding resource scopes. Revoking OAuth access does not
log out the Kratos account. This API supports public clients with S256; confidential
client secrets must never be included in browser bundles. Modern Web Crypto and
Fetch APIs are required (use a supported Node runtime or provide its Web Crypto).
Authentication capability discovery
With a server exposing GET /api/auth/capabilities:
const { data, error } = await client.auth.getCapabilities();
if (!data) throw error ?? new Error('Unable to discover capabilities');
const canUseOAuth = data.oauth.supported && data.oauth.enabled && data.oauth.available;This anonymous request preserves the configured base path and does not cache
capability state. enabled is the administrator choice; available is current
readiness, even when disabled. A disabled service can also be unavailable.
Use reason to explain an unavailable dependency, and check all three flags
before showing a usable entry point. OAuth resource access is supported by the matching backend build and defaults to disabled.
Discovery is advisory: protected endpoints still authorize each operation.
Older servers return a 404 error; normal Auth flows remain usable.
SDK 2.0 requires @sineforge/shared-schemas 2.1.0 or later in the 2.x series.
OAuth resource credentials
Enable instance OAuth resource access, set the client's resourceAccess to true,
and allow database:read, database:write, and optionally database:rpc. Request
those scopes with the instance's resourceAudience from /api/oauth/config:
const request = await oauth.createAuthorizationRequest(
['openid', 'offline_access', 'database:read', 'database:write', 'database:rpc'],
{ audience: 'https://login.example.com/resources' }
);
// Complete the one-use transaction using completeAuthorization as above.
const provider = oauth.createResourceTokenProvider(session);
const client = createClient({ baseUrl, resourceAccessTokenProvider: provider });
const result = await client.database.from('messages').select('*');
// Clear locally before attempting remote revocation.
const current = provider.getSession();
provider.clear();
if (current) await oauth.revoke(current);The provider uses memory only, merges concurrent refreshes, invalidates the matching token on resource HTTP 401, and prevents an in-flight refresh from undoing a local clear. The SDK does not replay a failed mutation on 401. A missing token fails without falling back to a cookie, API key, native session or anonymous key. Cookies are omitted, redirects are rejected, and the token goes only to the configured database and storage API paths. Realtime uses a separate explicit handshake credential. Unsupported modules reject this provider.
Custom providers implement getAccessToken(): Promise<string | null> and
invalidate(accessToken): void. They own refresh, expiry and logout handling.
Kratos state is independent; clearing or revoking OAuth does not sign out Kratos.
Database writes require read + write scopes; RPC requires all three database scopes.
First-party clients retain the ordinary user UUID, grants and RLS boundary.
OAuth files and realtime
Resource audience is the issuer's /resources. The earlier unpublished database
preview audience /resources/database is no longer accepted: save the application
again and request a new authorization. Scopes remain independently granted.
| Operation | Required scopes |
| -------------------------------- | ---------------------------------------- |
| List/download files | storage:read |
| Upload/replace/delete files | storage:read, storage:write |
| Connect/subscribe/receive events | realtime:subscribe |
| Publish events | realtime:subscribe, realtime:publish |
Storage requests reuse existing object RLS. OAuth uploads and downloads use the
backend's authenticated proxy, including on S3. download() returns a Blob;
createSignedUrl() reports an error, because OAuth does not mint a shareable link
that survives revocation. Never put an access token in an object URL. A transfer
already authorized and started may complete; new transfers are checked again.
Realtime uses auth.oauthAccessToken and Fetch polling with cookies omitted and
upgrades disabled. Browser WebSocket handshakes can otherwise include ambient
Kratos cookies. Existing Kratos clients retain their transport. The server checks
every subscription, publish and queued delivery, checks membership again before
channel delivery, disconnects at expiry, and revalidates idle sockets every 30
seconds plus any dependency timeout. No positive token check is cached.
The built-in provider notifies active sockets on refresh and clear. Refresh
reconnects and resubscribes; clear disconnects and clears subscriptions/presence.
Reload data on connect to recover missed events. A server authentication rejection
leaves the socket disconnected until the application signs in and connects again.
Custom providers may implement onChange(listener) returning an unsubscribe
function; events are refreshed and cleared. Without notifications, connected
clients check their provider every 15 seconds. Always clear at OAuth logout even
when remote revocation fails.
Configurable login and MFA
Explicit auth.getConfig() calls refresh the runtime configuration. Optional defaultLoginMethod is password, email_otp, sms_otp, passkey, oidc:<provider-id> or null; mfaRequirement is optional, enrolled or required. Older servers may omit both fields. Defaults choose presentation order; available methods and active challenges come from server flow nodes.
Preserve the current credential on AUTH_MFA_REQUIRED. With mandatory enrollment, start a settings flow to enroll a second factor, then complete login with { aal: 'aal2' } and retry the protected operation. Enrollment remains accessible to an unenrolled AAL1 user. Do not persist setup keys or codes. Existing OAuth tokens keep their separate lifecycle.
RP-initiated OAuth logout
Register an exact post-logout callback in Console; Hydra requires its scheme,
hostname and port to match a sign-in callback. Call
oauth.createLogoutRequest(session, postLogoutRedirectUri), save only the
returned transaction per browser tab, clear the application resource provider,
and navigate to url. The URL includes an ID token hint: never log or persist it.
On return, remove the callback query from history and consume the saved
OAuthLogoutTransaction before calling oauth.completeLogout(callbackUrl, transaction).
State, issuer/client, callback and ten-minute lifetime are validated. Handle
logout_rejected as cancellation. Replays after reload require a new transaction.
The helper does not maintain persistent transaction storage for the application.
This ends the Hydra login session and may clear the browser's shared OAuth login cookie. Kratos identity sign-in and access/refresh grants remain active. No-session logout can return immediately without a confirmation page. Use the separate identity logout and grant revocation APIs for those effects. Front/back-channel notifications and coordinated logout are described under OAuth client protocols below.
Refresh preserves the earlier ID token if no new ID token is returned.
OAuth client protocols
These protocols require a Sineforge backend with P3 support and shared-schemas 2.1.0 or later. SDK 2.0 includes the matching contract dependency.
oauth.getApplicationConfiguration()fetches the current public client config without cookies or tokens. Usemode,deviceAuthorizationandscopesto show applicable UI. The server independently enforces the configuration.oauth.createDeviceAuthorization(scopes, { audience? })returns an in-memory transaction withuserCode, verification URLs, expiry and polling interval. CallpollDeviceAuthorization(transaction)afternextPollAt; waitretryAfterfor pending/slow-down results. Network errors back off; denial and expiry terminate the transaction. Revoke an authorization that arrives after cancellation. Device consent is explicit even for first-party applications.- Confidential OAuth options accept
authentication: { method: 'client_secret_basic', secret }or{ method: 'private_key_jwt', key, kid }. Private keys must be RSA/SHA-256 signing CryptoKeys with at least 2048 bits. These options are server-only. Register public JWKs in Console; overlap old/new keys during migration. Secret rotation replaces the old secret immediately. createOAuthServiceClient({ issuer, clientId, apiBaseUrl, authentication })exposesclientCredentials()andgetServiceIdentity(session). This returns a service principal, never a Kratos user or auth session. Theservice:readpermission supports service identity verification only; user resource APIs reject machine credentials.verifyBackchannelLogout(token, consume)validates signed notifications on a server. Supply an atomic durableconsume(jti, expiresAt)returning false on replay; remove sessions matching the verifiedsidand/orsubject.verifyFrontchannelLogout(url, receiverUri, session)validates local browser session bindings. Browser restrictions can prevent front-channel delivery. Neither notification revokes access tokens by itself.client.auth.coordinatedLogout({ confirm: true, revokeGrants: true, endOAuthSessions: true, identitySessions: 'keep' | 'current' | 'all' })returns per-step status. On an OAuth failure, identity removal is skipped for retry. Local identity state clears only when that removal completes. Hydra's subject-wide removal does not send notifications (notifications: 'not_sent').redeemOAuthRegistration({ apiBaseUrl, ticket })is server-only and accepts an administrator-approved single-use ticket. Save the returned secret privately. A lost response may have consumed the ticket; inspect registrations before requesting a replacement, and never retry automatically.
The example repository includes a confidential Code + PKCE server and registration, service/private-key and device CLI examples. Production apps must replace its in-memory session/replay stores with durable storage. Hooks are configured per client in Console and can veto token issuance, not alter claims.
Optional resource permissions
SDK 2.0 includes identity-bound permission checks and administrator resource operations.
Discover availability with auth.getCapabilities() before displaying optional UI.
The backend enforces registered resources for database queries, private files and realtime;
applications continue using the normal SDK data APIs. Use authenticated download() for
registered private files, since shareable signed URLs cannot preserve revocation checks.
See Permissions for configuration and resource management.
