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
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ npm install @xedo/sdk
```ts
import { Xedo } from '@xedo/sdk';

const xedo = new Xedo({ apiKey: process.env.XEDO_API_KEY! }); // xdk_live_… or xdk_test_…
const xedo = new Xedo({ apiKey: process.env.XEDO_API_KEY! }); // opaque xdk_… key

await xedo.ping(); // validate the key
const { data, total } = await xedo.products.list({ perPage: 20 });
```

## ⚠️ Server-side only

The SDK sends your `xdk_live_…` key as a Bearer token. **Never import it into a
The SDK sends your `xdk_…` key as a Bearer token. **Never import it into a
browser bundle** — your key would be exposed to anyone. Call it from a server:
Next.js Server Components / Route Handlers, Express, workers, scripts. The
constructor throws if it detects a browser environment (override only if you
Expand All @@ -34,16 +34,33 @@ truly know what you are doing via `dangerouslyAllowBrowser`).
```ts
const xedo = new Xedo({
apiKey: process.env.XEDO_API_KEY!,
baseUrl: 'https://systems.xedoapp.com/marketplace', // default
baseUrl: 'https://systems.xedoapp.com/marketplace', // Production (default)
maxRetries: 4, // auto-retry on 429 (default)
timeoutMs: 30000, // per-request timeout
fetch: customFetch, // optional injection (tests, edge)
});

xedo.environment; // 'test' | 'live' | 'unknown' (from key prefix)
xedo.lastRateLimit; // { limit, remaining, reset } from the last call
```

### Production vs Sandbox

The **`baseUrl`** selects the environment:

| Environment | `baseUrl` |
|---|---|
| Production (default) | `https://systems.xedoapp.com/marketplace` |
| Sandbox | `https://systems.xedotestnet.space/marketplace` |

```ts
const sandbox = new Xedo({
apiKey: process.env.XEDO_API_KEY!,
baseUrl: 'https://systems.xedotestnet.space/marketplace',
});
```

See the [Environments guide](https://developers.xedoapp.com/introduction/environments) for details.

## Resources

| Resource | Methods |
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs-server-component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Next.js (App Router) Server Component. The Xedo client is instantiated and
* used entirely on the server, so the xdk_live_… key never reaches the browser.
* used entirely on the server, so the xdk_… key never reaches the browser.
*/
import { Xedo } from '@xedo/sdk';

Expand Down
4 changes: 2 additions & 2 deletions examples/script.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Plain Node script. Run with: XEDO_API_KEY=xdk_test_… npx tsx examples/script.ts
* Plain Node script. Run with: XEDO_API_KEY=xdk_… npx tsx examples/script.ts
*/
import { Xedo, XedoNotFoundError } from '@xedo/sdk';

Expand All @@ -8,7 +8,7 @@ const xedo = new Xedo({ apiKey: process.env.XEDO_API_KEY! });
async function main() {
// 1. Validate the key.
const ping = await xedo.ping();
console.log('marketplace', ping.marketplaceId, '(', xedo.environment, ')');
console.log('marketplace', ping.marketplaceId);

// 2. Inspect the merchant configuration and delivery areas.
const profile = await xedo.marketplace.retrieve();
Expand Down
24 changes: 8 additions & 16 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ const DEFAULT_MAX_RETRIES = 4;
const DEFAULT_TIMEOUT_MS = 30_000;

export interface XedoOptions {
/** Developer API key: `xdk_live_…` or `xdk_test_…`. */
/** Developer API key: an opaque `xdk_…` string. */
apiKey: string;
/** Override the API base URL. Defaults to the production marketplace URL. */
/**
* Override the API base URL. Defaults to the production marketplace URL.
* The base URL alone selects Production vs Sandbox — the key is opaque and
* carries no environment. See https://developers.xedoapp.com/introduction/environments
*/
baseUrl?: string;
/** Max automatic retries on `429`. Defaults to 4. */
maxRetries?: number;
Expand All @@ -25,7 +29,7 @@ export interface XedoOptions {
fetch?: FetchLike;
/**
* Escape hatch to run in a browser. Strongly discouraged: your
* `xdk_live_…` key would be exposed in the client bundle.
* `xdk_…` key would be exposed in the client bundle.
*/
dangerouslyAllowBrowser?: boolean;
}
Expand All @@ -47,7 +51,6 @@ export class Xedo {
readonly deliveryAreas: DeliveryAreas;
readonly marketplace: Marketplace;

private readonly apiKey: string;
private readonly transport: Transport;

constructor(options: XedoOptions) {
Expand All @@ -67,7 +70,7 @@ export class Xedo {
status: 0,
message:
'The Xedo SDK is server-side only: running it in a browser would expose your ' +
'xdk_live_… key in the client bundle. Call it from a server (Next.js Server ' +
'xdk_… key in the client bundle. Call it from a server (Next.js Server ' +
'Component / Route Handler, Express, a worker, …). See the README on authentication.',
});
}
Expand All @@ -82,7 +85,6 @@ export class Xedo {
});
}

this.apiKey = options.apiKey;
this.transport = new Transport({
apiKey: options.apiKey,
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
Expand All @@ -99,16 +101,6 @@ export class Xedo {
this.marketplace = new Marketplace(this.transport);
}

/**
* The environment inferred from the key prefix. The rest of the key is
* treated as opaque.
*/
get environment(): 'test' | 'live' | 'unknown' {
if (this.apiKey.startsWith('xdk_live_')) return 'live';
if (this.apiKey.startsWith('xdk_test_')) return 'test';
return 'unknown';
}

/** Rate-limit headers from the most recent response, for monitoring. */
get lastRateLimit(): RateLimitInfo | null {
return this.transport.lastRateLimit;
Expand Down
8 changes: 1 addition & 7 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ describe('Xedo client', () => {
expect(() => new Xedo({})).toThrow(/apiKey/i);
});

it('infers the environment from the key prefix', () => {
expect(makeClient({ apiKey: 'xdk_live_x' }).environment).toBe('live');
expect(makeClient({ apiKey: 'xdk_test_x' }).environment).toBe('test');
expect(makeClient({ apiKey: 'whatever' }).environment).toBe('unknown');
});

it('sends the Bearer auth header and unwraps `data` on ping', async () => {
let authHeader: string | null = null;
server.use(
Expand All @@ -28,7 +22,7 @@ describe('Xedo client', () => {
const xedo = makeClient();
const result = await xedo.ping();

expect(authHeader).toBe('Bearer xdk_test_abc123');
expect(authHeader).toBe('Bearer xdk_abc123');
expect(result).toEqual({ marketplaceId: 42, timestamp: '2026-05-28T10:15:00.000Z' });
});

Expand Down
2 changes: 1 addition & 1 deletion test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BASE } from './server';

export function makeClient(overrides: Partial<XedoOptions> = {}): Xedo {
return new Xedo({
apiKey: 'xdk_test_abc123',
apiKey: 'xdk_abc123',
baseUrl: BASE,
maxRetries: 2,
timeoutMs: 1000,
Expand Down
Loading