Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,3 @@ For Jest in this repo, `--runInBand` is often safer in constrained Windows envir
- Do not make social auth depend on dashboard login cookies; it belongs to project public auth.
- When touching docs, update both repo docs and in-dashboard docs if the user-facing flow changes.

## Current version: v0.8.0
Social auth (GitHub + Google) shipped. Next: v0.9.0 — Webhooks + BYOK Resend mail.

## Planned for v0.9.0 (already done)
- Webhook system: per-project config, HMAC-SHA256, retry, delivery logs
- BYOK Resend mail key: project-level Resend API key, custom domain mail
- Follow same encryption pattern as authProviders for storing Resend key
- Webhook model: separate MongoDB collection (not embedded in Project)
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "urbackend-monorepo",
"version": "0.8.0",
"version": "0.9.0",
"private": true,
"workspaces": [
"apps/*",
Expand Down
2 changes: 1 addition & 1 deletion sdks/urbackend-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@urbackend/sdk",
"version": "0.1.1",
"version": "0.2.0",
"description": "Official TypeScript SDK for urBackend BaaS",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
Expand Down
44 changes: 40 additions & 4 deletions sdks/urbackend-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,27 @@ import { UrBackendError, parseApiError } from './errors';
import { AuthModule } from './modules/auth';
import { DatabaseModule } from './modules/database';
import { StorageModule } from './modules/storage';
import { SchemaModule } from './modules/schema';
import { MailModule } from './modules/mail';

export class UrBackendClient {
private apiKey: string;
private baseUrl: string;
private _auth?: AuthModule;
private _db?: DatabaseModule;
private _storage?: StorageModule;
private _schema?: SchemaModule;
private _mail?: MailModule;
private headers: Record<string, string>;

constructor(config: UrBackendConfig) {
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.ub.bitbros.in';
this.headers = config.headers || {};

if (typeof window !== 'undefined') {
if (typeof window !== 'undefined' && this.apiKey.startsWith('sk_live_')) {
console.warn(
'⚠️ urbackend-sdk: Avoid exposing your SK-API key in client-side code(instead use pk_live key). This can lead to unauthorized access to your account and data.',
'⚠️ urbackend-sdk: Avoid exposing your Secret Key (sk_live_...) in client-side code. This can lead to unauthorized access to your account and data. Use your Publishable Key (pk_live_...) instead.',
);
}
}
Expand All @@ -45,6 +49,28 @@ export class UrBackendClient {
return this._storage;
}

get schema(): SchemaModule {
if (!this._schema) {
this._schema = new SchemaModule(this);
}
return this._schema;
}

get mail(): MailModule {
if (!this._mail) {
this._mail = new MailModule(this);
}
return this._mail;
}

public getBaseUrl(): string {
return this.baseUrl;
}

public getApiKey(): string {
return this.apiKey;
}

/**
* Internal request handler
*/
Expand All @@ -56,14 +82,19 @@ export class UrBackendClient {
const url = `${this.baseUrl}${path}`;
const headers: Record<string, string> = {
'x-api-key': this.apiKey,
'User-Agent': `urbackend-sdk-js/0.1.1`,
'User-Agent': `urbackend-sdk-js/0.2.0`,
...this.headers,
};

if (options.token) {
headers['Authorization'] = `Bearer ${options.token}`;
}

// Merge custom headers from options if provided
if (options.headers) {
Object.assign(headers, options.headers);
}

let requestBody: BodyInit | undefined;

if (options.isMultipart) {
Expand All @@ -79,6 +110,7 @@ export class UrBackendClient {
method,
headers,
body: requestBody,
credentials: options.credentials,
});

if (!response.ok) {
Expand All @@ -89,7 +121,11 @@ export class UrBackendClient {
if (contentType && contentType.includes('application/json')) {
const json = await response.json();
// The API returns { data, success, message }
return json.data !== undefined ? json.data : json;
// If data is present, return it. If success/message are present but no data, return the whole object (for exchange/logout etc)
if (json.data !== undefined) {
return json.data;
}
return json;
}

return (await response.text()) as unknown as T;
Expand Down
7 changes: 6 additions & 1 deletion sdks/urbackend-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { UrBackendClient } from './client';
import { UrBackendConfig } from './types';
import { AuthModule } from './modules/auth';
import { DatabaseModule } from './modules/database';
import { StorageModule } from './modules/storage';
import { SchemaModule } from './modules/schema';
import { MailModule } from './modules/mail';

export * from './types';
export * from './errors';
export { UrBackendClient };
export { UrBackendClient, AuthModule, DatabaseModule, StorageModule, SchemaModule, MailModule };

/**
* Factory function to create a new urBackend client
Expand Down
172 changes: 167 additions & 5 deletions sdks/urbackend-sdk/src/modules/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { UrBackendClient } from '../client';
import { AuthUser, AuthResponse, SignUpPayload, LoginPayload } from '../types';
import {
AuthUser,
AuthResponse,
SignUpPayload,
LoginPayload,
UpdateProfilePayload,
ChangePasswordPayload,
VerifyEmailPayload,
ResendOtpPayload,
RequestPasswordResetPayload,
ResetPasswordPayload,
SocialExchangePayload,
SocialExchangeResponse,
RequestOptions,
} from '../types';
import { AuthError } from '../errors';

export class AuthModule {
Expand All @@ -21,7 +35,15 @@ export class AuthModule {
const response = await this.client.request<AuthResponse>('POST', '/api/userAuth/login', {
body: payload,
});
this.sessionToken = response.token;

this.sessionToken = response.accessToken || response.token;

if (!response.accessToken && response.token) {
console.warn(
'urbackend-sdk: The server returned "token" which is deprecated. Please update your backend to return "accessToken".',
);
}

return response;
}

Expand All @@ -32,16 +54,156 @@ export class AuthModule {
const activeToken = token || this.sessionToken;

if (!activeToken) {
throw new AuthError('Authentication token is required for /me endpoint', 401, '/api/userAuth/me');
throw new AuthError(
'Authentication token is required for /me endpoint',
401,
'/api/userAuth/me',
);
}

return this.client.request<AuthUser>('GET', '/api/userAuth/me', { token: activeToken });
}

/**
* Clear the local session token
* Update the current authenticated user's profile
*/
public async updateProfile(payload: UpdateProfilePayload, token?: string): Promise<{ message: string }> {
const activeToken = token || this.sessionToken;
if (!activeToken) {
throw new AuthError('Authentication token is required to update profile', 401, '/api/userAuth/update-profile');
}
return this.client.request<{ message: string }>('PUT', '/api/userAuth/update-profile', {
body: payload,
token: activeToken,
});
}

/**
* Change the current authenticated user's password
*/
public async changePassword(payload: ChangePasswordPayload, token?: string): Promise<{ message: string }> {
const activeToken = token || this.sessionToken;
if (!activeToken) {
throw new AuthError('Authentication token is required to change password', 401, '/api/userAuth/change-password');
}
return this.client.request<{ message: string }>('PUT', '/api/userAuth/change-password', {
body: payload,
token: activeToken,
});
}

/**
* Verify user email with OTP
*/
public async verifyEmail(payload: VerifyEmailPayload): Promise<{ message: string }> {
return this.client.request<{ message: string }>('POST', '/api/userAuth/verify-email', {
body: payload,
});
}

/**
* Resend verification OTP
*/
public async resendVerificationOtp(payload: ResendOtpPayload): Promise<{ message: string }> {
return this.client.request<{ message: string }>('POST', '/api/userAuth/resend-verification-otp', {
body: payload,
});
}

/**
* Request password reset OTP
*/
public async requestPasswordReset(payload: RequestPasswordResetPayload): Promise<{ message: string }> {
return this.client.request<{ message: string }>('POST', '/api/userAuth/request-password-reset', {
body: payload,
});
}

/**
* Reset user password with OTP
*/
public logout(): void {
public async resetPassword(payload: ResetPasswordPayload): Promise<{ message: string }> {
return this.client.request<{ message: string }>('POST', '/api/userAuth/reset-password', {
body: payload,
});
}

/**
* Get public-safe profile by username
*/
public async publicProfile(username: string): Promise<AuthUser> {
return this.client.request<AuthUser>('GET', `/api/userAuth/public/${username}`);
}

/**
* Refresh the access token
* @param refreshToken Optional refresh token for header mode. If omitted, uses cookie mode.
*/
public async refreshToken(refreshToken?: string): Promise<AuthResponse> {
const options: RequestOptions = {};
if (refreshToken) {
options.headers = { 'x-refresh-token': refreshToken, 'x-refresh-token-mode': 'header' };
} else {
options.credentials = 'include';
}

const response = await this.client.request<AuthResponse>('POST', '/api/userAuth/refresh-token', options);
Comment on lines +142 to +150
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refreshToken() uses const options: any = {} and relies on a non-typed headers property. Since the client already has a RequestOptions type, this should be strongly typed (and RequestOptions should include an optional headers field) to avoid type holes and accidental misuse.

Copilot uses AI. Check for mistakes.
this.sessionToken = response.accessToken || response.token;
return response;
}

/**
* Returns the start URL for social authentication.
* Redirect the user's browser to this URL to begin the flow.
*/
public socialStart(provider: 'github' | 'google'): string {
return `${this.client.getBaseUrl()}/api/userAuth/social/${provider}/start?key=${this.client.getApiKey()}`;
}

/**
* Exchange social auth rtCode for a refresh token
*/
public async socialExchange(payload: SocialExchangePayload): Promise<SocialExchangeResponse> {
return this.client.request<SocialExchangeResponse>('POST', '/api/userAuth/social/exchange', {
body: payload,
});
}

/**
* Revoke the current session and clear local state
*/
public async logout(token?: string): Promise<{ success: boolean; message: string }> {
const activeToken = token || this.sessionToken;
let result = { success: true, message: 'Logged out locally' };

if (activeToken) {
try {
result = await this.client.request<{ success: boolean; message: string }>(
'POST',
'/api/userAuth/logout',
{ token: activeToken, credentials: 'include' },
);
} catch (e) {
// Silently fail if server logout fails, we still want to clear local state
console.warn('urbackend-sdk: Server logout failed', e);
}
}

this.sessionToken = undefined;
return result;
}

/**
* Manually set the session token (e.g. after social auth exchange)
*/
public setToken(token: string): void {
this.sessionToken = token;
}

/**
* Get the current stored session token
*/
public getToken(): string | undefined {
return this.sessionToken;
}
}
Loading
Loading