@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 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.
@@ -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
+ }