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
20 changes: 20 additions & 0 deletions .changeset/api-url-version-suffix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@nylas/connect": minor
---

Add automatic API URL version suffix handling

The NylasConnect client now automatically appends `/v3` to API URLs that don't already have a version suffix. This ensures all API calls use versioned endpoints while preserving any explicitly set versions.

**Features:**
- Automatically appends `/v3` to API URLs without version suffixes
- Preserves existing version suffixes (e.g., `/v1`, `/v2`, `/v10`)
- Handles trailing slashes correctly
- Works with custom API URLs and regional endpoints

**Examples:**
- `https://api.us.nylas.com` → `https://api.us.nylas.com/v3`
- `https://api.us.nylas.com/v2` → `https://api.us.nylas.com/v2` (unchanged)
- `https://custom.api.com` → `https://custom.api.com/v3`

This change is backward compatible and doesn't affect existing functionality.
126 changes: 126 additions & 0 deletions packages/nylas-connect/src/connect-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,3 +759,129 @@ describe("NylasConnect (sessions, validation, and events)", () => {
});
});
});

describe("NylasConnect (API URL normalization)", () => {
const clientId = "client_123";
const redirectUri = "https://app.example/callback";

beforeEach(() => {
localStorage.clear();
vi.restoreAllMocks();
});

it("should append /v3 to default API URL when no version is present", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v3/connect/auth");
});

it("should append /v3 to custom API URL when no version is present", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://custom.api.com",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://custom.api.com/v3/connect/auth");
});

it("should preserve existing version suffix (v2)", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com/v2",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v2/connect/auth");
expect(url).not.toContain("/v3");
});

it("should preserve existing version suffix (v1)", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com/v1",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v1/connect/auth");
expect(url).not.toContain("/v3");
});

it("should handle trailing slashes correctly", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com/",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v3/connect/auth");
expect(url).not.toContain("//v3"); // Should not have double slashes
});

it("should handle multiple trailing slashes correctly", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com///",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v3/connect/auth");
expect(url).not.toContain("//v3"); // Should not have double slashes
});

it("should preserve version with trailing slashes", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com/v2/",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v2/connect/auth");
expect(url).not.toContain("/v3");
});

it("should append /v3 to default URL when no apiUrl is provided", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
// No apiUrl provided - should use default
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v3/connect/auth");
});

it("should handle EU API URL correctly", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.eu.nylas.com",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.eu.nylas.com/v3/connect/auth");
});

it("should work with higher version numbers", async () => {
const auth = new NylasConnect({
clientId,
redirectUri,
apiUrl: "https://api.us.nylas.com/v10",
});

const { url } = await auth.getAuthUrl();
expect(url).toContain("https://api.us.nylas.com/v10/connect/auth");
expect(url).not.toContain("/v3");
});
});
18 changes: 17 additions & 1 deletion packages/nylas-connect/src/connect-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,35 @@ export class NylasConnect {
});
}

/**
* Normalize API URL to ensure it has a version suffix
*/
private normalizeApiUrl(apiUrl: string): string {
// Remove trailing slashes
const cleanUrl = apiUrl.replace(/\/+$/, "");
// Check if URL already has a version suffix (e.g., /v3, /v2, etc.)
const versionPattern = /\/v\d+$/;
if (versionPattern.test(cleanUrl)) {
return cleanUrl;
}
// Append /v3 if no version suffix is present
return `${cleanUrl}/v3`;
}

/**
* Resolve configuration with environment variables and smart defaults
*/
private resolveConfig(config: ConnectConfig): ConnectConfig {
const environment = this.detectEnvironment(config.environment);
const baseApiUrl = config.apiUrl || "https://api.us.nylas.com";

return {
clientId: config.clientId || this.getEnvVar("NYLAS_CLIENT_ID"),
redirectUri:
config.redirectUri ||
this.getEnvVar("NYLAS_REDIRECT_URI") ||
this.detectRedirectUri(),
apiUrl: config.apiUrl || "https://api.us.nylas.com",
apiUrl: this.normalizeApiUrl(baseApiUrl),
environment,
defaultScopes: config.defaultScopes,
debug: config.debug ?? environment === "development",
Expand Down