@aetherwealth/sdk 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/client.d.ts +130 -0
- package/dist/client.js +429 -0
- package/dist/errors.d.ts +131 -0
- package/dist/errors.js +178 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/resources/accounts.d.ts +8 -0
- package/dist/resources/accounts.js +84 -0
- package/dist/resources/alerts.d.ts +8 -0
- package/dist/resources/alerts.js +178 -0
- package/dist/resources/diary.d.ts +8 -0
- package/dist/resources/diary.js +75 -0
- package/dist/resources/envelope.d.ts +7 -0
- package/dist/resources/envelope.js +15 -0
- package/dist/resources/idempotency.d.ts +15 -0
- package/dist/resources/idempotency.js +19 -0
- package/dist/resources/market.d.ts +8 -0
- package/dist/resources/market.js +90 -0
- package/dist/resources/paginate.d.ts +15 -0
- package/dist/resources/paginate.js +41 -0
- package/dist/resources/stats.d.ts +8 -0
- package/dist/resources/stats.js +28 -0
- package/dist/resources/trades.d.ts +8 -0
- package/dist/resources/trades.js +113 -0
- package/dist/resources/validate.d.ts +12 -0
- package/dist/resources/validate.js +22 -0
- package/dist/retry.d.ts +40 -0
- package/dist/retry.js +110 -0
- package/dist/schemas.d.ts +286 -0
- package/dist/schemas.js +239 -0
- package/dist/types.d.ts +528 -0
- package/dist/types.js +12 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aether Wealth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @aetherwealth/sdk
|
|
2
|
+
|
|
3
|
+
Typed TypeScript client for the **Aether Wealth** public API. Programmatic
|
|
4
|
+
access to your trading journal, accounts, analytics, alerts, market data, and
|
|
5
|
+
diary over the public REST surface (`/api/public/v1/…`).
|
|
6
|
+
|
|
7
|
+
> **Keep your API key secret.** Authentication uses a public API key
|
|
8
|
+
> (`aw_live_…`) sent as a bearer token. Treat it like a password — never commit
|
|
9
|
+
> it or ship it to an untrusted client.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @aetherwealth/sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { AetherClient } from '@aetherwealth/sdk'
|
|
21
|
+
|
|
22
|
+
const client = new AetherClient({
|
|
23
|
+
auth: { type: 'apiKey', apiKey: process.env.AETHER_API_KEY! },
|
|
24
|
+
// baseUrl defaults to the production API (https://api.aetherwealth.ai).
|
|
25
|
+
// Set it only to target staging or a local dev backend.
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Every call is authenticated with your API key.
|
|
29
|
+
const { data: openTrades } = await client.trades.list({ status: 'OPEN' })
|
|
30
|
+
const stats = await client.stats.summary({ pair: 'EURUSD' })
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Authentication
|
|
34
|
+
|
|
35
|
+
Each request sends your public API key as `Authorization: Bearer <apiKey>`. The
|
|
36
|
+
key scopes every operation to its owner — you never pass `userId` in a request
|
|
37
|
+
body. `auth` is a discriminated union (one member today, `apiKey`) so future
|
|
38
|
+
auth modes can be added without breaking existing callers.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
new AetherClient({
|
|
42
|
+
baseUrl,
|
|
43
|
+
auth: { type: 'apiKey', apiKey },
|
|
44
|
+
fetchImpl, // optional — inject a custom fetch (tests, proxies)
|
|
45
|
+
userAgent, // optional
|
|
46
|
+
timeoutMs: 30_000, // optional — per-request timeout (default 30s; 0 disables)
|
|
47
|
+
validateResponses: false, // optional — opt-in Zod validation of responses
|
|
48
|
+
dangerouslyAllowBrowser: false, // optional — see "Browser use" below
|
|
49
|
+
onRequest, onResponse, // optional diagnostics hooks
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The diagnostics hooks receive only `{ method, url, status, ms }` — your API key
|
|
54
|
+
travels solely on the `Authorization` header and never appears in a hook
|
|
55
|
+
payload, a thrown error, or the request URL.
|
|
56
|
+
|
|
57
|
+
### Browser use is blocked by default
|
|
58
|
+
|
|
59
|
+
The API key is a **server-side secret**. Constructing the client in a
|
|
60
|
+
browser-like environment (`window.document` present) throws — a key shipped to a
|
|
61
|
+
browser is visible in the bundle and DevTools and must be treated as public.
|
|
62
|
+
Call the API from your server. If you are certain you are in a trusted
|
|
63
|
+
non-browser runtime that happens to define `window`, set
|
|
64
|
+
`dangerouslyAllowBrowser: true` (mirrors the OpenAI SDK).
|
|
65
|
+
|
|
66
|
+
## Resources
|
|
67
|
+
|
|
68
|
+
| Resource | Methods |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `client.trades` | `list` · `get` · `create` · `update` · `close` · `delete` · `pages` · `listAll` |
|
|
71
|
+
| `client.accounts` | `list` · `create` · `update` · `delete` · `trades` · `tradesAll` |
|
|
72
|
+
| `client.stats` | `summary` |
|
|
73
|
+
| `client.alerts` | `list` · `createPrice` · `createTrendline` · `update` · `delete` · `listIndicator` · `createIndicator` · `updateIndicator` · `deleteIndicator` |
|
|
74
|
+
| `client.market` | `config` · `calendar` · `macroSeries` · `macro` |
|
|
75
|
+
| `client.diary` | `list` · `get` · `upsert` · `delete` · `pages` · `listAll` |
|
|
76
|
+
|
|
77
|
+
List methods return `{ data, pagination }`; single-item methods return the
|
|
78
|
+
object directly. All response fields are camelCase.
|
|
79
|
+
|
|
80
|
+
## Pagination
|
|
81
|
+
|
|
82
|
+
`pages()` and `listAll()` async-iterate a paginated resource, auto-advancing
|
|
83
|
+
`page` until the last page (or an empty page), with an infinite-loop backstop:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// One page at a time (keeps pagination metadata):
|
|
87
|
+
for await (const page of client.trades.pages({ status: 'CLOSED', limit: 100 })) {
|
|
88
|
+
console.log(page.pagination.page, page.data.length)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Every item, flattened across all pages:
|
|
92
|
+
for await (const trade of client.trades.listAll({ pair: 'EURUSD' })) {
|
|
93
|
+
handle(trade)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Also: client.diary.pages/listAll and client.accounts.tradesAll(accountId)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Iteration starts at `query.page` if given, else page 1.
|
|
100
|
+
|
|
101
|
+
## Timeouts & cancellation
|
|
102
|
+
|
|
103
|
+
Every request is bounded by `timeoutMs` (default 30s), overridable per call. A
|
|
104
|
+
timeout aborts the request and rejects with `AetherTimeoutError` (a subclass of
|
|
105
|
+
`AetherNetworkError`, so it's retryable and caught by existing network-error
|
|
106
|
+
handlers). You can also pass your own `AbortSignal` — the request aborts when
|
|
107
|
+
either the timeout or your signal fires:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// Client-wide default (30s) or a per-request override via the low-level request():
|
|
111
|
+
const slow = new AetherClient({ baseUrl, auth, timeoutMs: 60_000 })
|
|
112
|
+
await client.request('/api/public/v1/trades/list', { method: 'POST', body: {}, timeoutMs: 5_000 })
|
|
113
|
+
|
|
114
|
+
// Caller cancellation (e.g. a UI "cancel" button or request budget):
|
|
115
|
+
const controller = new AbortController()
|
|
116
|
+
setTimeout(() => controller.abort(new Error('cancelled')), 1_000)
|
|
117
|
+
await client.request('/api/public/v1/stats/stats', { method: 'POST', body: {}, signal: controller.signal })
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A timeout throws `AetherTimeoutError`; a caller-initiated abort surfaces your
|
|
121
|
+
signal's own reason (it is *not* treated as a transient error to retry).
|
|
122
|
+
|
|
123
|
+
## Idempotency & safe retries
|
|
124
|
+
|
|
125
|
+
Each create — `trades.create`, `accounts.create`, `alerts.createPrice`,
|
|
126
|
+
`alerts.createTrendline`, `alerts.createIndicator` — sends an `Idempotency-Key`
|
|
127
|
+
header so a create that the server committed but whose response you never saw
|
|
128
|
+
won't duplicate on a re-send. If you don't pass one, the SDK generates a fresh
|
|
129
|
+
key **per call**:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
await client.trades.create(input) // auto Idempotency-Key, protects this one call
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
An auto per-call key protects a single invocation. It does **not** make a create
|
|
136
|
+
retry-safe: a new key each attempt means the backend can't dedup. To retry a
|
|
137
|
+
create safely, pass a **stable** key so every attempt targets the same record:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { withRetry } from '@aetherwealth/sdk'
|
|
141
|
+
|
|
142
|
+
const idempotencyKey = crypto.randomUUID() // generated ONCE, outside the retry
|
|
143
|
+
const trade = await withRetry(
|
|
144
|
+
() => client.trades.create(input, { idempotencyKey }),
|
|
145
|
+
{ maxAttempts: 3 },
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> Do **not** generate the key inside the retried closure — each attempt would
|
|
150
|
+
> get a different key and dedup would be lost. `withRetry` only retries
|
|
151
|
+
> transient failures (`AetherRateLimitError`, `AetherNetworkError`,
|
|
152
|
+
> `AetherTimeoutError`); retrying a mutation is only safe with a stable key.
|
|
153
|
+
|
|
154
|
+
## Errors
|
|
155
|
+
|
|
156
|
+
Non-2xx responses (and `{ success: false }` envelopes) throw a typed
|
|
157
|
+
`AetherApiError` subclass so you can branch on the failure mode:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import {
|
|
161
|
+
AetherRateLimitError,
|
|
162
|
+
AetherNotFoundError,
|
|
163
|
+
AetherAuthError,
|
|
164
|
+
AetherTimeoutError,
|
|
165
|
+
} from '@aetherwealth/sdk'
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await client.trades.get(id)
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err instanceof AetherNotFoundError) { /* 404 */ }
|
|
171
|
+
else if (err instanceof AetherRateLimitError) { /* 429 — err.retryAfterSeconds */ }
|
|
172
|
+
else if (err instanceof AetherAuthError) { /* 401 — invalid or missing API key */ }
|
|
173
|
+
else if (err instanceof AetherTimeoutError) { /* client timeout — err.timeoutMs / err.elapsedMs */ }
|
|
174
|
+
else throw err
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`AetherTimeoutError` and `AetherNetworkError` (its parent) never reached the
|
|
179
|
+
server; they carry no HTTP status. All HTTP failures are `AetherApiError`
|
|
180
|
+
subclasses.
|
|
181
|
+
|
|
182
|
+
## Optional runtime validation
|
|
183
|
+
|
|
184
|
+
Compile-time types come from the SDK. Runtime validation is **off by default**
|
|
185
|
+
for performance and additive-field tolerance (a backend that adds a field won't
|
|
186
|
+
break older SDK consumers). Turn it on two ways:
|
|
187
|
+
|
|
188
|
+
Whole-client — every resource validates its response with a Zod parser and
|
|
189
|
+
throws a `ZodError` on shape drift (unknown fields are still tolerated):
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const client = new AetherClient({ baseUrl, auth, validateResponses: true })
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Per-call — import a parser and validate a single response:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { parseTrade } from '@aetherwealth/sdk'
|
|
199
|
+
const trade = parseTrade(await client.trades.get(id)) // throws on shape drift
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Not covered
|
|
203
|
+
|
|
204
|
+
Live candles and AI chat conversations are streamed/tRPC-only on the backend
|
|
205
|
+
and are intentionally **not** part of this REST SDK — reach them through
|
|
206
|
+
`@aetherwealth/client-core` (used by the CLI and MCP server). API-key
|
|
207
|
+
management is likewise out of scope: an API key can't provision other keys.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { AccountsResource, AlertsResource, DiaryResource, MarketResource, StatsResource, TradesResource } from './types.js';
|
|
2
|
+
export type AetherFetchInit = {
|
|
3
|
+
method?: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
|
|
4
|
+
query?: Record<string, string | number | boolean | undefined | null | string[]>;
|
|
5
|
+
body?: unknown;
|
|
6
|
+
/** Per-request timeout override (ms). Falls back to the client's `timeoutMs`. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Caller-supplied cancellation signal, composed with the internal timeout —
|
|
10
|
+
* the request aborts when EITHER fires. A caller-initiated abort surfaces as
|
|
11
|
+
* the signal's reason; a timeout throws {@link AetherTimeoutError}.
|
|
12
|
+
*/
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** When set, sent as the `Idempotency-Key` header so a retried create dedups. */
|
|
15
|
+
idempotencyKey?: string;
|
|
16
|
+
};
|
|
17
|
+
/** API-key auth: a public key (`aw_live_…`) sent as `Authorization: Bearer`. */
|
|
18
|
+
export type ApiKeyAuth = {
|
|
19
|
+
type: 'apiKey';
|
|
20
|
+
apiKey: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Supported auth modes. A discriminated union with one member today; keeping
|
|
24
|
+
* it a union means an `oauth`/`hmac` member can be added later without changing
|
|
25
|
+
* this type's shape or breaking callers that pass `{type: 'apiKey', …}`.
|
|
26
|
+
*/
|
|
27
|
+
export type AetherAuth = ApiKeyAuth;
|
|
28
|
+
export type AetherClientConfig = {
|
|
29
|
+
/**
|
|
30
|
+
* Backend base URL. Optional — defaults to the production API
|
|
31
|
+
* ({@link DEFAULT_BASE_URL}). Only set it to target staging or a local dev
|
|
32
|
+
* backend (e.g. `http://127.0.0.1:9006`).
|
|
33
|
+
*/
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
/** Auth credential — required. API-key mode sends `Authorization: Bearer`. */
|
|
36
|
+
auth: AetherAuth;
|
|
37
|
+
/** Override the global `fetch`. Defaults to `globalThis.fetch`. */
|
|
38
|
+
fetchImpl?: typeof fetch;
|
|
39
|
+
/** Optional default `User-Agent`. */
|
|
40
|
+
userAgent?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Default per-request timeout in ms (default {@link DEFAULT_TIMEOUT_MS}).
|
|
43
|
+
* A request that exceeds this is aborted and rejects with
|
|
44
|
+
* {@link AetherTimeoutError}. Set `0` (or a non-finite value) to disable.
|
|
45
|
+
* Overridable per call via `request({timeoutMs})`.
|
|
46
|
+
*/
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Escape hatch to run in a browser-like environment. Default `false` — the
|
|
50
|
+
* constructor throws in a browser because the API key is a server secret
|
|
51
|
+
* and any bundled/DevTools-visible key is effectively public. Only enable
|
|
52
|
+
* for a trusted non-browser runtime that happens to define `window`.
|
|
53
|
+
*/
|
|
54
|
+
dangerouslyAllowBrowser?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* When `true`, each resource runs its Zod parser over the response before
|
|
57
|
+
* returning (throwing `ZodError` on shape drift). Default `false` for
|
|
58
|
+
* performance and additive-field tolerance.
|
|
59
|
+
*/
|
|
60
|
+
validateResponses?: boolean;
|
|
61
|
+
/** Optional per-request diagnostics hook (e.g. debug logging). */
|
|
62
|
+
onRequest?: (req: {
|
|
63
|
+
method: string;
|
|
64
|
+
url: string;
|
|
65
|
+
}) => void;
|
|
66
|
+
onResponse?: (res: {
|
|
67
|
+
method: string;
|
|
68
|
+
url: string;
|
|
69
|
+
status: number;
|
|
70
|
+
ms: number;
|
|
71
|
+
}) => void;
|
|
72
|
+
};
|
|
73
|
+
/** Default per-request timeout budget (ms). */
|
|
74
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
75
|
+
/** Production public API base URL — used when `baseUrl` is omitted. */
|
|
76
|
+
export declare const DEFAULT_BASE_URL = "https://api.aetherwealth.ai";
|
|
77
|
+
export declare class AetherClient {
|
|
78
|
+
readonly trades: TradesResource;
|
|
79
|
+
readonly accounts: AccountsResource;
|
|
80
|
+
readonly stats: StatsResource;
|
|
81
|
+
readonly alerts: AlertsResource;
|
|
82
|
+
readonly market: MarketResource;
|
|
83
|
+
readonly diary: DiaryResource;
|
|
84
|
+
/**
|
|
85
|
+
* When `true`, resources validate responses with their Zod parser before
|
|
86
|
+
* returning. Exposed (readonly) so resource factories can read it.
|
|
87
|
+
*/
|
|
88
|
+
readonly validateResponses: boolean;
|
|
89
|
+
private readonly config;
|
|
90
|
+
private readonly fetchImpl;
|
|
91
|
+
private readonly timeoutMs;
|
|
92
|
+
constructor(config: AetherClientConfig);
|
|
93
|
+
/**
|
|
94
|
+
* Low-level request used by resource implementations and as an escape hatch
|
|
95
|
+
* for routes the SDK doesn't model. Returns the parsed `data` field when the
|
|
96
|
+
* envelope has one (e.g. `{success, data}`), otherwise the whole parsed
|
|
97
|
+
* object (so resources can `pluck` their named key). Throws a typed
|
|
98
|
+
* `AetherApiError` subclass on any non-2xx or `{success: false}`.
|
|
99
|
+
*/
|
|
100
|
+
request<T>(path: string, init?: AetherFetchInit): Promise<T>;
|
|
101
|
+
/**
|
|
102
|
+
* Classify a thrown request error, in priority order:
|
|
103
|
+
* (a) caller-initiated cancellation → re-throw the caller's reason as-is
|
|
104
|
+
* (unwrapped, non-retryable: `withRetry` must not retry a cancel);
|
|
105
|
+
* (b) our internal timeout fired → {@link AetherTimeoutError};
|
|
106
|
+
* (c) a typed API error from `parseResponse` (4xx / `{success:false}`) →
|
|
107
|
+
* propagate unchanged (an application error, not a transport/abort one);
|
|
108
|
+
* (d) anything else → {@link AetherNetworkError} (DNS/TCP/TLS, stalled read).
|
|
109
|
+
*/
|
|
110
|
+
private throwRequestError;
|
|
111
|
+
/**
|
|
112
|
+
* Turn a fetched `Response` into the resolved value or a typed throw:
|
|
113
|
+
* classifies non-2xx / `{success:false}`, unwraps a literal `data` envelope,
|
|
114
|
+
* and otherwise returns the whole parsed object for the resource to `pluck`.
|
|
115
|
+
*/
|
|
116
|
+
private parseResponse;
|
|
117
|
+
/**
|
|
118
|
+
* Redact the API key from the parsed error body, then classify it into the
|
|
119
|
+
* most specific {@link AetherApiError} subtype. Redaction happens HERE — the
|
|
120
|
+
* single choke point for turning a response body into a thrown error — so a
|
|
121
|
+
* key echoed back by a proxy/WAF can never reach `err.message` or
|
|
122
|
+
* `err.responseBody`. The redacted body is a fresh structure; `parsed` (the
|
|
123
|
+
* caller's success-path value) is left untouched.
|
|
124
|
+
*/
|
|
125
|
+
private throwClassifiedError;
|
|
126
|
+
/** The API key to scrub from error bodies (empty when auth carries no key). */
|
|
127
|
+
private redactionSecret;
|
|
128
|
+
private buildHeaders;
|
|
129
|
+
private tick;
|
|
130
|
+
}
|