Skip to content
Closed
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
6 changes: 4 additions & 2 deletions examples/web-cli/api/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { signCredentials, getSigningSecret } from "../server/sign-handler.js";
* Request Body:
* - apiKey: string (required) - Ably API key in format "appId.keyId:secret"
* - bypassRateLimit: boolean (optional) - Set to true for CI/testing
* - endpoint: string (optional) - Custom Ably endpoint URL
* - controlAPIHost: string (optional) - Custom control API host URL
*
* Response:
* - signedConfig: string - JSON-encoded config that was signed
Expand All @@ -35,14 +37,14 @@ export default async function handler(
return res.status(500).json({ error: "Signing secret not configured" });
}

const { apiKey, bypassRateLimit } = req.body;
const { apiKey, bypassRateLimit, endpoint, controlAPIHost } = req.body;

if (!apiKey) {
return res.status(400).json({ error: "apiKey is required" });
}

// Use shared signing logic
const result = signCredentials({ apiKey, bypassRateLimit }, secret);
const result = signCredentials({ apiKey, bypassRateLimit, endpoint, controlAPIHost }, secret);

res.status(200).json(result);
}
6 changes: 4 additions & 2 deletions examples/web-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@ably/react-web-cli": "workspace:*",
Expand Down Expand Up @@ -35,6 +36,7 @@
"typescript": "~5.7.2",
"typescript-eslint": "^8.24.1",
"vite": "^6.2.7",
"vite-tsconfig-paths": "^5.1.4"
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.0"
}
}
118 changes: 118 additions & 0 deletions examples/web-cli/server/__tests__/sign-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect } from "vitest";
import { signCredentials } from "../sign-handler";

describe("signCredentials()", () => {
const TEST_SECRET = "test-secret-key";

describe("endpoint configuration", () => {
it("should include endpoint in signed config when provided", () => {
const { signedConfig } = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
endpoint: "https://custom.ably.io",
},
TEST_SECRET
);

const config = JSON.parse(signedConfig);
expect(config.endpoint).toBe("https://custom.ably.io");
});

it("should include controlAPIHost in signed config when provided", () => {
const { signedConfig } = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
controlAPIHost: "https://control-api.ably.io",
},
TEST_SECRET
);

const config = JSON.parse(signedConfig);
expect(config.controlAPIHost).toBe("https://control-api.ably.io");
});

it("should include both endpoint and controlAPIHost when provided", () => {
const { signedConfig } = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
endpoint: "https://custom.ably.io",
controlAPIHost: "https://control-api.ably.io",
},
TEST_SECRET
);

const config = JSON.parse(signedConfig);
expect(config.endpoint).toBe("https://custom.ably.io");
expect(config.controlAPIHost).toBe("https://control-api.ably.io");
});

it("should not include endpoint/controlAPIHost when not provided", () => {
const { signedConfig } = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
},
TEST_SECRET
);

const config = JSON.parse(signedConfig);
expect(config.endpoint).toBeUndefined();
expect(config.controlAPIHost).toBeUndefined();
});

it("should include standard fields along with endpoint config", () => {
const { signedConfig } = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: true,
endpoint: "https://custom.ably.io",
},
TEST_SECRET
);

const config = JSON.parse(signedConfig);
expect(config.apiKey).toBe("test-key");
expect(config.bypassRateLimit).toBe(true);
expect(config.timestamp).toBeDefined();
expect(config.endpoint).toBe("https://custom.ably.io");
});

it("should generate valid signature with endpoint in config", () => {
const result = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
endpoint: "https://custom.ably.io",
},
TEST_SECRET
);

expect(result.signature).toBeDefined();
expect(result.signature).toHaveLength(64); // HMAC-SHA256 produces 64-char hex string
});

it("should produce different signatures with and without endpoint", () => {
const withEndpoint = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
endpoint: "https://custom.ably.io",
},
TEST_SECRET
);

const withoutEndpoint = signCredentials(
{
apiKey: "test-key",
bypassRateLimit: false,
},
TEST_SECRET
);

expect(withEndpoint.signature).not.toBe(withoutEndpoint.signature);
});
});
});
6 changes: 5 additions & 1 deletion examples/web-cli/server/sign-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import crypto from "crypto";
export interface SignRequest {
apiKey: string;
bypassRateLimit?: boolean;
endpoint?: string;
controlAPIHost?: string;
}

export interface SignResponse {
Expand All @@ -25,13 +27,15 @@ export function signCredentials(
request: SignRequest,
secret: string,
): SignResponse {
const { apiKey, bypassRateLimit } = request;
const { apiKey, bypassRateLimit, endpoint, controlAPIHost } = request;

// Build config object (matches terminal server expectations)
const config = {
apiKey,
timestamp: Date.now(),
bypassRateLimit: bypassRateLimit || false,
...(endpoint && { endpoint }),
...(controlAPIHost && { controlAPIHost }),
};

// Serialize to JSON - this exact string is what gets signed
Expand Down
9 changes: 8 additions & 1 deletion examples/web-cli/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,20 @@ function App() {
// Handle authentication
const handleAuthenticate = useCallback(async (newApiKey: string, remember?: boolean) => {
try {
// Optional: Get endpoint configuration from environment variables
// Real implementations should determine these values based on their requirements
const endpoint = import.meta.env.VITE_ABLY_ENDPOINT;
const controlAPIHost = import.meta.env.VITE_ABLY_CONTROL_HOST;

// Call /api/sign endpoint to get signed config
const response = await fetch('/api/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apiKey: newApiKey,
bypassRateLimit: false
bypassRateLimit: false,
...(endpoint && { endpoint }),
...(controlAPIHost && { controlAPIHost }),
})
});

Expand Down
4 changes: 2 additions & 2 deletions examples/web-cli/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function apiSignPlugin(): Plugin {

req.on("end", () => {
try {
const { apiKey, bypassRateLimit } = JSON.parse(body);
const { apiKey, bypassRateLimit, endpoint, controlAPIHost } = JSON.parse(body);

if (!apiKey) {
res.statusCode = 400;
Expand All @@ -46,7 +46,7 @@ function apiSignPlugin(): Plugin {
}

// Use shared signing logic
const result = signCredentials({ apiKey, bypassRateLimit }, secret);
const result = signCredentials({ apiKey, bypassRateLimit, endpoint, controlAPIHost }, secret);

res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
Expand Down
10 changes: 10 additions & 0 deletions examples/web-cli/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
globals: true,
environment: "node",
watch: false,
testTimeout: 10000,
},
});
Loading
Loading