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
44 changes: 22 additions & 22 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,26 @@ export class ObjectStackClient {
*/
async connect() {
this.logger.debug('Connecting to ObjectStack server', { baseUrl: this.baseUrl });

try {
let data: DiscoveryResult | undefined;

// 1. Try Standard Discovery (.well-known)
// 1. Try Protocol-standard Discovery Path /api/v1/discovery (primary)
try {
const discoveryUrl = `${this.baseUrl}/api/v1/discovery`;
this.logger.debug('Probing protocol-standard discovery endpoint', { url: discoveryUrl });
const res = await this.fetchImpl(discoveryUrl);
Comment on lines +262 to +264
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

discoveryUrl is built via string concatenation with baseUrl (${this.baseUrl}/api/v1/discovery). If baseUrl contains a path segment (e.g. https://host/some/prefix), this will probe .../some/prefix/api/v1/discovery, which is inconsistent with the .well-known logic (which correctly uses new URL(this.baseUrl).origin). Consider constructing the protocol-standard discovery URL using the parsed origin as well (and falling back to a root-relative /api/v1/discovery when baseUrl isn’t absolute).

Copilot uses AI. Check for mistakes.
if (res.ok) {
const body = await res.json();
data = body.data || body;
this.logger.debug('Discovered via /api/v1/discovery');
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

In the primary /api/v1/discovery probe, non-2xx responses are silently ignored (only exceptions are logged). This makes it hard to diagnose cases like 401/404/500 where the request succeeds but returns an error status. Consider adding a debug log when res.ok is false (include status/statusText) so users can see why the primary discovery path was skipped.

Suggested change
this.logger.debug('Discovered via /api/v1/discovery');
this.logger.debug('Discovered via /api/v1/discovery');
} else {
this.logger.debug('Protocol-standard discovery endpoint returned non-success status', {
url: discoveryUrl,
status: res.status,
statusText: res.statusText
});

Copilot uses AI. Check for mistakes.
}
} catch (e) {
this.logger.debug('Protocol-standard discovery probe failed', { error: (e as Error).message });
}

// 2. Fallback to Standard Discovery (.well-known)
if (!data) {
let wellKnownUrl: string;
try {
// If baseUrl is absolute, get origin
Expand All @@ -269,24 +283,10 @@ export class ObjectStackClient {
wellKnownUrl = '/.well-known/objectstack';
}

this.logger.debug('Probing .well-known discovery', { url: wellKnownUrl });
this.logger.debug('Falling back to .well-known discovery', { url: wellKnownUrl });
const res = await this.fetchImpl(wellKnownUrl);
if (res.ok) {
const body = await res.json();
data = body.data || body;
this.logger.debug('Discovered via .well-known');
}
} catch (e) {
this.logger.debug('Standard discovery probe failed', { error: (e as Error).message });
}

// 2. Fallback to Protocol-standard Discovery Path /api/v1/discovery
if (!data) {
const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
this.logger.debug('Falling back to standard discovery endpoint', { url: fallbackUrl });
const res = await this.fetchImpl(fallbackUrl);
if (!res.ok) {
throw new Error(`Failed to connect to ${fallbackUrl}: ${res.statusText}`);
throw new Error(`Failed to connect to ${wellKnownUrl}: ${res.statusText}`);
}
const body = await res.json();
data = body.data || body;
Expand All @@ -297,13 +297,13 @@ export class ObjectStackClient {
}

this.discoveryInfo = data;
this.logger.info('Connected to ObjectStack server', {

this.logger.info('Connected to ObjectStack server', {
version: data.version,
apiName: data.apiName,
services: data.services
services: data.services
});

return data as DiscoveryResult;
} catch (e) {
this.logger.error('Failed to connect to ObjectStack server', e as Error, { baseUrl: this.baseUrl });
Expand Down
10 changes: 5 additions & 5 deletions packages/client/tests/integration/01-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import { ObjectStackClient } from '../../src/index';
const TEST_SERVER_URL = process.env.TEST_SERVER_URL || 'http://localhost:3000';

describe('Discovery & Connection', () => {
describe('TC-DISC-001: Standard Discovery via .well-known', () => {
test('should discover API from .well-known/objectstack', async () => {
const client = new ObjectStackClient({
describe('TC-DISC-001: Protocol-standard Discovery via /api/v1/discovery', () => {
test('should discover API from /api/v1/discovery', async () => {
const client = new ObjectStackClient({
baseUrl: TEST_SERVER_URL,
debug: true
});

const discovery = await client.connect();
Comment on lines +16 to 23
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

This test was renamed to claim protocol-standard discovery via /api/v1/discovery, but it doesn’t assert that connect() actually used that endpoint (it would still pass if the server only supports the .well-known fallback). Consider wrapping fetch with a spy in the test to assert the first attempted URL is /api/v1/discovery, and add a companion test that forces /api/v1/discovery to fail and verifies fallback to /.well-known/objectstack.

Copilot uses AI. Check for mistakes.

expect(discovery.version).toBeDefined();
expect(discovery.apiName).toBeDefined();
expect(discovery.routes).toBeDefined();
Expand Down
Loading