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
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,36 @@ pnpm add @relatiohq/opencloud
yarn add @relatiohq/opencloud
```

## Usage
## Quick Start

```typescript
import { OpenCloud } from "@relatiohq/opencloud";

// With API key authentication
const client = new OpenCloud({
apiKey: "your-api-key",
});

// Use the client
const user = await client.users.get("123456789");
```

### Per-Request Authentication (OAuth2)

Perfect for multi-tenant applications:

```typescript
// Create client without default auth
const client = new OpenCloud();

// Use different credentials per request
const userClient = client.withAuth({
kind: "oauth2",
accessToken: "user-access-token",
});

const groups = await userClient.groups.listGroupMemberships("123456");
```

## Documentation

Full API documentation is available at [https://opencloud.relatio.cc](https://opencloud.relatio.cc)
Expand All @@ -42,6 +59,7 @@ Full API documentation is available at [https://opencloud.relatio.cc](https://op
- Tree-shakeable exports
- Lightweight with zero dependencies
- Automatic retry with exponential backoff
- Flexible authentication

## Why Use This SDK?

Expand Down
109 changes: 105 additions & 4 deletions src/docs/guide/authentication.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Authentication

The OpenCloud SDK requires a Roblox Open Cloud API key for authentication.
The OpenCloud SDK supports two authentication methods: API keys and OAuth2 access tokens.

## Getting an API Key
## API Key Authentication

API keys are best for server-side applications that need to access Roblox resources on behalf of your application.

### Getting an API Key

1. Go to the [Roblox Creator Dashboard](https://create.roblox.com/dashboard/credentials)
2. Click **Create API Key**
Expand All @@ -13,7 +17,7 @@ The OpenCloud SDK requires a Roblox Open Cloud API key for authentication.
Keep your API key secret! Never commit it to version control or share it publicly.
:::

## Using the API Key
### Using the API Key

Pass your API key when creating the OpenCloud client:

Expand All @@ -25,6 +29,77 @@ const client = new OpenCloud({
});
```

## OAuth2 Authentication

OAuth2 is ideal for applications that need to access Roblox resources on behalf of individual users. This is commonly used in multi-tenant scenarios where each user has their own access token.

### Per-Request Authentication

You can create a client without default credentials and provide authentication per-request using the `withAuth()` method:

```typescript
import { OpenCloud } from "@relatiohq/opencloud";

// Create a client without default authentication
const client = new OpenCloud();

// Use OAuth2 for a specific request
const userClient = client.withAuth({
kind: "oauth2",
accessToken: "user-access-token"
});

const groups = await userClient.groups.listGroupMemberships("123456");
```

### Multi-Tenant Server Example

This pattern is perfect for backend services that handle requests from multiple users:

```typescript
import { OpenCloud } from "@relatiohq/opencloud";
import express from "express";

const app = express();
const client = new OpenCloud(); // Shared client, no default auth

app.get("/api/my-groups", async (req, res) => {
// Get user's access token from request
const accessToken = req.headers.authorization?.split(" ")[1];

// Create scoped client for this user
const userClient = client.withAuth({
kind: "oauth2",
accessToken: accessToken!
});

// Make requests on behalf of the user
const memberships = await userClient.groups.listGroupMemberships(req.query.userId);
res.json(memberships);
});
```

### Overriding Default Authentication

You can also create a client with default authentication and override it per-request:

```typescript
// Client with default API key
const client = new OpenCloud({
apiKey: "default-api-key"
});

// Most requests use the default API key
const group = await client.groups.get("123");

// But you can override with OAuth2 for specific requests
const userClient = client.withAuth({
kind: "oauth2",
accessToken: "user-token"
});
const userGroups = await userClient.groups.listGroupMemberships("456");
```

## Environment Variables

It's recommended to store your API key in environment variables:
Expand Down Expand Up @@ -54,6 +129,32 @@ const client = new OpenCloud({
});
```

## Authentication Types

The SDK supports two authentication types through the `AuthConfig` type:

### API Key

```typescript
const auth = {
kind: "apiKey",
apiKey: "your-api-key"
};

const client = new OpenCloud().withAuth(auth);
```

### OAuth2

```typescript
const auth = {
kind: "oauth2",
accessToken: "user-access-token"
};

const client = new OpenCloud().withAuth(auth);
```

## Permissions

When creating your API key, ensure it has the appropriate permissions for the resources you need to access:
Expand All @@ -63,4 +164,4 @@ When creating your API key, ensure it has the appropriate permissions for the re
- **Assets** - Upload and manage assets
- And more...

Refer to the [Roblox Open Cloud documentation](https://create.roblox.com/docs/cloud/open-cloud) for the full list of available permissions.
Refer to the [Roblox Open Cloud documentation](https://create.roblox.com/docs/cloud/open-cloud) for the full list of available permissions.
17 changes: 17 additions & 0 deletions src/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { OpenCloudError, RateLimitError, AuthError } from "./errors";
import type { AuthConfig } from "./types";

export type Backoff = "exponential" | "fixed";

Expand Down Expand Up @@ -50,6 +51,8 @@ const parseErrorDetails = async (
*/
export class HttpClient {
private fetcher: typeof fetch;
/** Optional auth override for per-request authentication */
public authOverride?: AuthConfig;

/**
* Creates a new HttpClient instance.
Expand Down Expand Up @@ -110,6 +113,20 @@ export class HttpClient {
...init.headers,
});

if (this.authOverride) {
if (this.authOverride.kind === "apiKey") {
headers.set("x-api-key", this.authOverride.apiKey);
} else {
headers.set("authorization", `Bearer ${this.authOverride.accessToken}`);
}
}

if (!headers.has("x-api-key") && !headers.has("authorization")) {
throw new AuthError(
"No authentication provided. Pass an apiKey in OpenCloudConfig or use withAuth() for per-request authentication.",
);
}

for (let attempt = 0; attempt <= retry.attempts; attempt++) {
const response = await this.fetcher(url, { ...init, headers });

Expand Down
76 changes: 67 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { HttpClient, HttpOptions } from "./http";
import { Groups } from "./resources/groups";
import { Users } from "./resources/users";
import type { AuthConfig } from "./types/auth";

/**
* Configuration options for initializing the OpenCloud SDK client.
*/
export interface OpenCloudConfig {
/** Roblox Open Cloud API key for authentication */
apiKey: string;
/**
* Roblox Open Cloud API key for authentication.
* This is optional and if not provided, you must use withAuth() for per-request authentication.
*/
apiKey?: string;
/** Custom user agent string for API requests (defaults to "@relatiohq/opencloud") */
userAgent?: string;
/** Base URL for the Roblox API (defaults to "https://apis.roblox.com") */
Expand All @@ -24,42 +28,96 @@ export interface OpenCloudConfig {
*
* @example
* ```typescript
* // Traditional usage with API key
* const client = new OpenCloud({
* apiKey: 'your-api-key-here'
* });
*
* const user = await client.users.get('123456789');
* console.log(user.displayName);
*
* // Multi-tenant usage with per-request OAuth2
* const client = new OpenCloud(); // No default auth
* const userClient = client.withAuth({
* kind: "oauth2",
* accessToken: "user-token"
* });
* const groups = await userClient.groups.listMemberships("123456");
* ```
*/
export class OpenCloud {
/** Access to Users API endpoints */
public users: Users;
public groups: Groups;
public users!: Users;
public groups!: Groups;
private http!: HttpClient;

/**
* Creates a new OpenCloud SDK client instance.
*
* @param config - Configuration options for the client
*/
constructor(config: OpenCloudConfig) {
const http = new HttpClient({
constructor(config: OpenCloudConfig = {}) {
this.http = new HttpClient({
baseUrl: config.baseUrl ?? "https://apis.roblox.com",
retry: config.retry ?? {
attempts: 4,
backoff: "exponential",
baseMs: 250,
},
headers: {
"x-api-key": config.apiKey,
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
"user-agent": config.userAgent ?? "@relatiohq/opencloud",
},
fetchImpl: config.fetchImpl,
});

this.initializeResources(this.http);
}

/**
* Initializes all resource endpoints with the provided HTTP client.
* @private
*/
private initializeResources(http: HttpClient): void {
this.users = new Users(http);
this.groups = new Groups(http);
}

/**
* Creates a scoped client instance with per-request authentication.
* This allows reusing a single client while providing different credentials for each request.
*
* @param auth - Authentication configuration to use for this scope
* @returns A new scoped OpenCloud instance with the provided auth
*
* @example
* ```typescript
* const client = new OpenCloud(); // No default auth
*
* // OAuth2 authentication
* const userClient = client.withAuth({
* kind: "oauth2",
* accessToken: "user-token-here"
* });
* const groups = await userClient.groups.listMemberships("123456");
*
* // API key authentication
* const adminClient = client.withAuth({
* kind: "apiKey",
* apiKey: "admin-key-here"
* });
* const user = await adminClient.users.get("789");
* ```
*/
withAuth(auth: AuthConfig): OpenCloud {
const scoped = Object.create(this) as OpenCloud;

const scopedHttp = Object.create(this.http) as HttpClient;
scopedHttp.authOverride = auth;

scoped.http = scopedHttp;
scoped.initializeResources(scopedHttp);

return scoped;
}
}

export * from "./types";
Expand Down
17 changes: 17 additions & 0 deletions src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Authentication configuration for API requests.
* Supports both API key and OAuth2 bearer token authentication.
*/
export type AuthConfig =
| {
/** Authentication method using an API key */
kind: "apiKey";
/** The API key value */
apiKey: string;
}
| {
/** Authentication method using OAuth2 */
kind: "oauth2";
/** The OAuth2 access token */
accessToken: string;
};
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./auth";
export * from "./common";
export * from "./users";
export * from "./groups";
Loading