@wheelium/client 0.0.0-stage → 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ The SDK has independent versions from the CLI and the private web application.
4
+ Default SemVer: **patch** for fixes only; **minor** for backward-compatible
5
+ capabilities or installation/onboarding/distribution improvements; **major** for
6
+ breaking API, transport, authentication, supported-runtime or wire-type behavior.
7
+ Update this changelog for each release. A manifest version is not publication.
8
+
9
+ ## 0.1.0 — 2026-10-09
10
+
11
+ - Browser/Node >=24 ESM fetch client, shared by web app, integrations and CLI.
12
+ - Typed existing read, tracker/share/order, settings, invitation and key methods.
13
+ - Dynamic session token provider or static scoped-key credential, origin-bound
14
+ authenticated transport and explicitly unauthenticated public snapshots.
15
+ - Structured safe errors, abort/overall timeout, bounded safe-read retries,
16
+ preserved nullable/date/money wire types; no automatic mutation retries.
17
+ - Generated declarations, server-derived synthetic wire fixtures, OpenAPI drift
18
+ validation and clean local pack/install verification.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Guttman
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 CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # @wheelium/client
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Small ESM client for supported browsers and Node >=24, using native `fetch`. This package has no runtime dependencies and does not read environment variables, storage or browser globals. It is not a login/OAuth SDK. Package publication is a separate release step.
4
+
5
+ Install with `npm install @wheelium/client` once the initial registry release is published.
6
+ Use Node >=24. Publication is verified against npm, not inferred from a manifest.
7
+ The package version and release history are in [CHANGELOG.md](CHANGELOG.md).
8
+
9
+ Source [API contract](https://github.com/davidguttman/trade-tracker/blob/main/docs/reference/scoped-api.md)
10
+ and [integration guide](https://github.com/davidguttman/trade-tracker/blob/main/docs/scoped-api-integrations.md)
11
+ require repository access: the application's source repository is private.
12
+ The public package includes its usage documentation and generated declarations.
13
+ Independent release policy/history: [CHANGELOG.md](CHANGELOG.md), [LICENSE](LICENSE).
14
+
15
+ ## Local source and packed installation
16
+
17
+ With Node 24 on PATH, from repository root: `npm run test:api-contract`,
18
+ `npm run test:sdk`, `node examples/scoped-client.mjs --synthetic`, and
19
+ `npm run pack:public`. The last command prints the exact SDK/CLI tarball paths in
20
+ a unique TEMP directory. In a new consumer directory run `npm init -y`, then
21
+ `npm install --offline --ignore-scripts --no-audit --no-fund /exact/printed/wheelium-client-0.1.0.tgz`.
22
+ Import `@wheelium/client` there. Install both printed tarballs together if using
23
+ the CLI, whose SDK dependency is exact. No registry publication/global install is
24
+ performed. The source example's `--synthetic` uses fake metadata/money/empty trades
25
+ without network access; actual controlled reads require an explicit runtime key
26
+ and managed development base from the caller's own secret/config adapter.
27
+
28
+ ```js
29
+ import { createClient, WheeliumError } from '@wheelium/client'
30
+ const wheelium = createClient({ apiKey: 'YOUR_KEY' })
31
+ const identity = await wheelium.me()
32
+ const portfolio = await wheelium.portfolio.get({ includeGreeks: true })
33
+ const quotes = await wheelium.market.quotes({ optionSymbols: ['AAPL270115P00200000'] })
34
+ ```
35
+
36
+ Provide **exactly one** of `apiKey` or sync/async `getToken`. The provider runs for every request/attempt; returning null/undefined intentionally makes an unauthenticated request (useful for session adapters). Wheelium's server, not this package, decides authorization. Browser key management requires a session; existing session-only methods do not become API-key capabilities merely because they are exposed here.
37
+
38
+ Default `baseUrl`: `https://api.wheelium.app`. Explicit HTTPS development origins (including jump.sh) are supported. Relative bases are resolved by the browser adapter before creating the client; the library never accesses `window`. HTTP is allowed only for explicit loopback (`localhost`, `127.0.0.1`, `[::1]`) development/test bases. Embedded URL credentials and fragments are rejected. Authenticated requests cannot escape their configured base or follow redirects. `shares.publicSnapshot(httpsStorageUrl)` is explicitly unauthenticated, omits cookies, and rejects redirects; it never sends bearer credentials to storage.
39
+
40
+ Domain groups: `trades`, `portfolio`, `market`, `advisor`, `account`, `orders`, `shares`, `settingsAlpaca`, `invitations`, `keys`, and `backtest`; `me()` returns identity/scopes/broker metadata. Methods preserve server JSON exactly: dates stay strings, nullable values stay null, and broker account cash/buying power stay strings. Portfolio math stays on the server. Generated declarations document each method's input/output and support typed consumers. Flexible provider fields are represented as unknown, not `any`.
41
+
42
+ ## Failures, cancellation and retries
43
+
44
+ `WheeliumError` has `status` (0 for transport failures), `code`, `requestId`, and `requiredScopes`. HTTP payloads preserve safe legacy error messages; non-JSON error bodies are never exposed. Stable transport codes include `network_error`, `invalid_response`, `timeout`, `aborted`, `unsafe_url`, and `unsafe_redirect`. Errors do not retain raw response bodies, headers or credentials. There is no SDK logging. UI redirects/logout are exclusively the web adapter's behavior.
45
+
46
+ All methods accept a final `{ signal }` option. `timeoutMs` (default 30 seconds) bounds the entire operation, including dynamic token resolution, response body consumption and retries. Aborted body readers are cancelled and timers/listeners are cleaned up.
47
+
48
+ Only explicitly classified ordinary reads and the quote/earnings batch POST reads retry transient network failures or 408/429/500/502/503/504. Default `maxRetries` is 2; supported range 0–3. `Retry-After` is respected; otherwise exponential delays include jitter (`retryDelayMs` defaults to 250). The overall timeout still bounds waiting. 401/403 never retry. Backtests, credential testing, uploads, tracker mutations, publication and all order mutations never automatically retry, even if a caller passes a retry flag.
49
+
50
+ **Order outcome can be uncertain:** a timeout, network error or failed response does not prove the broker rejected an order. Reconcile the existing broker order/account/trade state (and `clientOrderId` where supported) before resubmitting. The library does not promise exactly-once execution or automatic idempotency. `orders.list()` is the existing equity-order listing; no option-order listing endpoint is invented.
51
+
52
+ `request(path, options)` is a transitional typed low-level escape hatch. `rawResponse: true` returns a buffered Response for legacy consumers, covering body consumption with the same timeout. JSON calls return undefined for 204/empty bodies. Passing FormData preserves its multipart boundary rather than forcing JSON Content-Type.
53
+
54
+ ## Repository verification
55
+
56
+ From the repository root: `npm run client:types`, `npm run test:client-types`, and `npm run test:sdk`. Declaration artifacts are generated from the JS public entry's JSDoc and `types.ts` wire contracts. `test:client-types` detects source/declaration drift and validates typed usage, including forbidden credential combinations and incorrect money/order inputs. The web app imports the same `packages/client/index.js` entry that this package exports; no root workspace or file dependency is required.
57
+
58
+ Declaration drift checks compare generated files to handwritten types; they do not prove the server contract. `test/client/wire.test.mjs` separately executes the actual server trade/position builders and earnings assembly with synthetic offline inputs, pins their JSON against typed fixtures, and checks SDK pass-through. Read trades may have null fills/tickers or missing persisted metadata; pair trades may come from broker positions without an ID. Write inputs and persisted get/create/update results keep their stricter record types. Earnings history remains an array of date strings.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Exactly one credential source; token providers are evaluated for each attempt.
3
+ * Only domain methods explicitly marked safe may retry (never mutations/backtests).
4
+ * @param {ClientOptions} options
5
+ * @returns {WheeliumClient}
6
+ */
7
+ export function createClient(options: ClientOptions): WheeliumClient;
8
+ /** @import { RequestOptions } from './types.js' */
9
+ /** An HTTP or transport failure. Bodies, headers and credentials are never retained. */
10
+ export class WheeliumError extends Error {
11
+ /** @param {string} message @param {{status?: number, code?: string, requestId?: string, requiredScopes?: string[]}} [details] */
12
+ constructor(message: string, details?: {
13
+ status?: number;
14
+ code?: string;
15
+ requestId?: string;
16
+ requiredScopes?: string[];
17
+ });
18
+ status: number;
19
+ code: string;
20
+ requestId: string | undefined;
21
+ requiredScopes: string[];
22
+ }
23
+ export type ClientOptions = import("./types.js").ClientOptions;
24
+ export type WheeliumClient = import("./types.js").WheeliumClient;
25
+ export type Trade = import("./types.js").Trade;
26
+ export type Scope = import("./types.js").Scope;
27
+ export type CallOptions = import("./types.js").CallOptions;
@@ -0,0 +1,664 @@
1
+ export type Scope = 'trades:read' | 'portfolio:read' | 'market:read' | 'advisor:read' | 'account:read' | 'orders:read' | 'shares:read' | 'backtests:run' | 'trades:write' | 'shares:write' | 'orders:write' | 'orders:cancel';
2
+ export type BrokerMode = 'paper' | 'live';
3
+ export type OptionType = 'PUT' | 'CALL';
4
+ export type TradeAction = 'STO' | 'BTC' | 'ASSIGN' | 'CALLED' | 'SELL_STOCK' | 'BUY_STOCK';
5
+ export interface WireObject {
6
+ [key: string]: unknown;
7
+ }
8
+ export interface CallOptions {
9
+ signal?: AbortSignal;
10
+ }
11
+ export interface RequestOptions extends CallOptions {
12
+ method?: string;
13
+ headers?: HeadersInit;
14
+ body?: BodyInit;
15
+ authenticated?: boolean;
16
+ rawResponse?: boolean;
17
+ safeRetry?: boolean;
18
+ }
19
+ export type ClientOptions = {
20
+ baseUrl?: string;
21
+ fetch?: typeof fetch;
22
+ timeoutMs?: number;
23
+ maxRetries?: number;
24
+ retryDelayMs?: number;
25
+ } & ({
26
+ apiKey: string;
27
+ getToken?: never;
28
+ } | {
29
+ apiKey?: never;
30
+ getToken: () => string | null | undefined | Promise<string | null | undefined>;
31
+ });
32
+ export interface Me {
33
+ type: 'session' | 'api_key';
34
+ userId: string;
35
+ email: string;
36
+ scopes: Scope[];
37
+ expiresAt: string | null;
38
+ broker: {
39
+ configured: boolean;
40
+ mode: BrokerMode | null;
41
+ allowedMode: BrokerMode | null;
42
+ };
43
+ }
44
+ export interface Greeks {
45
+ delta: number | null;
46
+ gamma: number | null;
47
+ theta: number | null;
48
+ vega: number | null;
49
+ }
50
+ export interface TradeInput {
51
+ symbol: string;
52
+ action: TradeAction;
53
+ date: string;
54
+ contracts: number;
55
+ fillPrice: number;
56
+ strike: number;
57
+ type?: OptionType | null;
58
+ expiration?: string | null;
59
+ optionSymbol?: string | null;
60
+ ticker?: string;
61
+ notes?: string | null;
62
+ source?: string;
63
+ alpacaOrderId?: string | null;
64
+ clientOrderId?: string | null;
65
+ underlyingPrice?: number | null;
66
+ greeks?: Greeks | null;
67
+ impliedVolatility?: number | null;
68
+ }
69
+ export interface ReadTrade extends WireObject {
70
+ symbol: string;
71
+ action: TradeAction;
72
+ date: string;
73
+ contracts: number;
74
+ strike: number;
75
+ fillPrice: number | null;
76
+ type?: OptionType | null;
77
+ expiration?: string | null;
78
+ optionSymbol?: string | null;
79
+ ticker?: string | null;
80
+ notes?: string | null;
81
+ source?: string;
82
+ userKey?: string | null;
83
+ premium?: number | null;
84
+ leverage?: number;
85
+ underlyingPrice?: number | null;
86
+ greeks?: Greeks | null;
87
+ impliedVolatility?: number | null;
88
+ _id?: string;
89
+ side?: 'buy' | 'sell';
90
+ positionEffect?: 'open' | 'close';
91
+ dateCreated?: string | null;
92
+ dateUpdated?: string | null;
93
+ alpacaStatus?: string | null;
94
+ alpacaOrderId?: string | null;
95
+ clientOrderId?: string | null;
96
+ }
97
+ export interface PersistedTrade extends ReadTrade {
98
+ _id: string;
99
+ fillPrice: number;
100
+ side: 'buy' | 'sell';
101
+ positionEffect: 'open' | 'close';
102
+ dateCreated: string;
103
+ dateUpdated: string;
104
+ }
105
+ export interface BrokerOrderTrade extends ReadTrade {
106
+ _id: string;
107
+ source: 'alpaca';
108
+ action: 'STO' | 'BTC';
109
+ type: OptionType;
110
+ expiration: string;
111
+ optionSymbol: string;
112
+ side: 'buy' | 'sell';
113
+ positionEffect: 'open' | 'close';
114
+ dateCreated: string | null;
115
+ dateUpdated: string | null;
116
+ limitPrice: number | null;
117
+ filledContracts: number;
118
+ requestedContracts: number | null;
119
+ isPending: boolean;
120
+ alpacaFilledAt: string | null;
121
+ }
122
+ export interface BrokerAssignmentTrade extends ReadTrade {
123
+ _id: string;
124
+ source: 'alpaca-activity';
125
+ action: 'ASSIGN' | 'CALLED';
126
+ fillPrice: 0;
127
+ }
128
+ export type Trade = PersistedTrade | BrokerOrderTrade | BrokerAssignmentTrade;
129
+ export interface BrokerPositionTrade extends ReadTrade {
130
+ source: 'alpaca-position';
131
+ action: 'STO' | 'BTC';
132
+ fillPrice: number;
133
+ type: OptionType;
134
+ expiration: string;
135
+ ticker: string;
136
+ premium: number;
137
+ leverage: number;
138
+ }
139
+ export type PairTrade = Trade | BrokerPositionTrade;
140
+ export interface Pair extends WireObject {
141
+ ticker?: string | null;
142
+ duration: number | null;
143
+ totalPL: number | null;
144
+ returnPercent: number | null;
145
+ dailyReturn: number | null;
146
+ maxRisk: number;
147
+ annualizedReturn: number | null;
148
+ openTrade: PairTrade;
149
+ closeTrade: PairTrade | null;
150
+ stockPrice?: number | null;
151
+ btcAsk?: number | null;
152
+ btcBid?: number | null;
153
+ currentDTE?: number | null;
154
+ }
155
+ export interface PortfolioPair extends WireObject {
156
+ ticker: string;
157
+ duration: number;
158
+ totalPL: number;
159
+ returnPercent: number;
160
+ dailyReturn: number;
161
+ maxRisk: number;
162
+ annualizedReturn?: number;
163
+ startTrade?: Trade;
164
+ exitTrade?: Trade;
165
+ }
166
+ export interface StockPosition extends WireObject {
167
+ symbol: string;
168
+ qty?: string | number;
169
+ quantity?: number;
170
+ shares: number;
171
+ costBasis: number | null;
172
+ totalCost?: number | null;
173
+ currentPrice?: number | null;
174
+ marketValue?: number | null;
175
+ unrealizedPL?: number | null;
176
+ unrealizedPLPercent?: number | null;
177
+ }
178
+ export interface PortfolioPoint extends WireObject {
179
+ date: string;
180
+ pairNumber: number;
181
+ currentPair: PortfolioPair;
182
+ totalPairs: number;
183
+ cashBalance: number;
184
+ cumulativeProfit: number;
185
+ portfolioDailyProfit: number;
186
+ annualizedProfit: number;
187
+ annualizedReturn: number;
188
+ avgLeverage: number;
189
+ }
190
+ export interface DailyPL extends WireObject {
191
+ dailyData: Array<WireObject & {
192
+ date: string;
193
+ cumulativePL: number;
194
+ optionsRealizedPL: number;
195
+ stockRealizedPL: number;
196
+ stockHoldingsPL: number;
197
+ totalPL: number;
198
+ }>;
199
+ avgCapitalAtRisk: number;
200
+ stockHoldings: number;
201
+ }
202
+ export interface ContractQuery {
203
+ symbol: string;
204
+ expirationDateGte?: string;
205
+ expirationDateLte?: string;
206
+ strikeMin?: number;
207
+ strikeMax?: number;
208
+ type?: 'call' | 'put' | 'both';
209
+ includeQuotes?: boolean;
210
+ includeGreeks?: boolean;
211
+ }
212
+ export interface Contract extends WireObject {
213
+ symbol: string;
214
+ strike_price?: string;
215
+ expiration_date?: string;
216
+ type?: 'call' | 'put';
217
+ greeks?: Greeks | null;
218
+ }
219
+ export interface OptionQuote extends WireObject {
220
+ optionSymbol: string;
221
+ underlyingSymbol: string | null;
222
+ underlyingPrice: number | null;
223
+ timestamp: string | null;
224
+ last: number | null;
225
+ askSize: number | null;
226
+ bidSize: number | null;
227
+ greeksSource: string | null;
228
+ greeksAsOf: string | null;
229
+ greeksStale: boolean;
230
+ bid?: number | null;
231
+ ask?: number | null;
232
+ mid?: number | null;
233
+ greeks?: Greeks | null;
234
+ impliedVolatility?: number | null;
235
+ }
236
+ export interface Earnings extends WireObject {
237
+ symbol: string;
238
+ available?: boolean;
239
+ message?: string;
240
+ upcoming?: {
241
+ date: string;
242
+ daysUntil: number;
243
+ confirmed: boolean;
244
+ time: string | null;
245
+ } | null;
246
+ historical?: string[];
247
+ }
248
+ export interface EquityOrderInput {
249
+ symbol: string;
250
+ side: 'buy' | 'sell';
251
+ qty: number | string;
252
+ }
253
+ export interface OptionsOrderInput {
254
+ symbol: string;
255
+ limitPrice: number;
256
+ quantity?: number;
257
+ clientOrderId?: string;
258
+ action?: 'STO' | 'BTC';
259
+ underlyingPrice?: number;
260
+ greeks?: Greeks | null;
261
+ impliedVolatility?: number;
262
+ }
263
+ export interface RollOrderInput {
264
+ closeSymbol: string;
265
+ openSymbol: string;
266
+ limitPrice: number;
267
+ quantity?: number;
268
+ clientOrderId?: string;
269
+ }
270
+ export interface BrokerOrder extends WireObject {
271
+ id: string;
272
+ status: string;
273
+ symbol?: string;
274
+ qty?: string;
275
+ filled_qty?: string;
276
+ filled_avg_price?: string | null;
277
+ client_order_id?: string;
278
+ }
279
+ export interface OptionsOrderResult {
280
+ status: string;
281
+ orderId: string;
282
+ clientOrderId: string;
283
+ filledQuantity: number;
284
+ fillPrice: number | null;
285
+ limitPrice: number;
286
+ raw: BrokerOrder;
287
+ legs?: {
288
+ symbol: string;
289
+ side: string;
290
+ intent: string;
291
+ }[];
292
+ }
293
+ export interface AlpacaConfig {
294
+ configured: boolean;
295
+ endpoint: BrokerMode | null;
296
+ keyIdLast4: string | null;
297
+ }
298
+ export interface AlpacaCredentials {
299
+ endpoint: BrokerMode;
300
+ keyId: string;
301
+ secretKey: string;
302
+ }
303
+ export interface ShareMetadata {
304
+ id: string;
305
+ pageType: 'portfolio' | 'positions';
306
+ createdAt: string;
307
+ shareUrl: string;
308
+ imageUrl?: string;
309
+ }
310
+ export interface Snapshot extends WireObject {
311
+ pageType: 'portfolio' | 'positions';
312
+ data: WireObject;
313
+ createdAt: string;
314
+ redacted: true;
315
+ }
316
+ export interface KeyMetadata {
317
+ id: string;
318
+ prefix: string;
319
+ name: string;
320
+ scopes: Scope[];
321
+ brokerMode: BrokerMode;
322
+ createdAt: string;
323
+ expiresAt: string | null;
324
+ revokedAt: string | null;
325
+ lastUsedAt: string | null;
326
+ }
327
+ export interface KeyInput {
328
+ name: string;
329
+ scopes?: Scope[];
330
+ expiresInDays?: 30 | 90 | 365 | null;
331
+ brokerMode?: BrokerMode;
332
+ }
333
+ export interface Signal extends WireObject {
334
+ action: string;
335
+ urgency: 'high' | 'medium' | 'low' | 'none';
336
+ reason?: string;
337
+ recommendation?: string;
338
+ metrics?: WireObject & {
339
+ symbol?: string;
340
+ strike?: number;
341
+ btcAsk?: number | null;
342
+ totalPL?: number | null;
343
+ ivRank?: number | null;
344
+ };
345
+ }
346
+ export interface RollCandidate extends WireObject {
347
+ ticker: string;
348
+ strike: number;
349
+ expiration: string;
350
+ type: OptionType;
351
+ dte: number;
352
+ delta: number | null;
353
+ bid: number;
354
+ ask: number | null;
355
+ mid: number | null;
356
+ impliedVolatility: number | null;
357
+ netCredit: number;
358
+ costToClose: number;
359
+ newPremium: number;
360
+ annualizedReturn: number;
361
+ }
362
+ export interface AdvisorPosition extends WireObject {
363
+ position: Pair;
364
+ signal: Signal;
365
+ }
366
+ export interface IVR extends WireObject {
367
+ symbol: string;
368
+ source: string;
369
+ stats: {
370
+ minIV: number;
371
+ maxIV: number;
372
+ avgIV: number;
373
+ dataPoints?: number;
374
+ dataDate?: string;
375
+ currentIV: number | null;
376
+ ivRank: number | null;
377
+ };
378
+ history: {
379
+ date: string;
380
+ avgIV: number;
381
+ contracts?: number;
382
+ ivr?: number | null;
383
+ }[];
384
+ interpretation: {
385
+ level: 'high' | 'moderate' | 'low';
386
+ description: string;
387
+ recommendation: string;
388
+ } | null;
389
+ }
390
+ export interface BacktestQuery {
391
+ symbol: string;
392
+ start: string;
393
+ end: string;
394
+ strategy?: string;
395
+ deltaMin?: number | string;
396
+ deltaMax?: number | string;
397
+ dteMin?: number | string;
398
+ dteMax?: number | string;
399
+ profitTarget?: number | string;
400
+ minIvr?: number | string;
401
+ minVolume?: number | string;
402
+ }
403
+ export interface WheeliumClient {
404
+ request<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
405
+ me(options?: CallOptions): Promise<Me>;
406
+ trades: {
407
+ list(options?: CallOptions): Promise<Trade[]>;
408
+ get(id: string, options?: CallOptions): Promise<PersistedTrade>;
409
+ pairs(params?: {
410
+ includeGreeks?: boolean;
411
+ includeStockHoldings?: boolean;
412
+ }, options?: CallOptions): Promise<Pair[] | {
413
+ pairs: Pair[];
414
+ stockPositions: StockPosition[];
415
+ }>;
416
+ create(body: TradeInput, options?: CallOptions): Promise<PersistedTrade>;
417
+ update(id: string, body: Partial<TradeInput>, options?: CallOptions): Promise<PersistedTrade>;
418
+ delete(id: string, options?: CallOptions): Promise<void>;
419
+ cancelBrokerOrder(id: string, options?: CallOptions): Promise<{
420
+ success: boolean;
421
+ message: string;
422
+ raw?: unknown;
423
+ }>;
424
+ };
425
+ portfolio: {
426
+ get(params?: {
427
+ includeGreeks?: boolean;
428
+ comparison?: string;
429
+ }, options?: CallOptions): Promise<PortfolioPoint[]>;
430
+ dailyPL(params?: {
431
+ comparison?: string;
432
+ }, options?: CallOptions): Promise<DailyPL>;
433
+ };
434
+ market: {
435
+ contracts(params: ContractQuery, options?: CallOptions): Promise<{
436
+ symbol: string;
437
+ count: number;
438
+ contracts: Contract[];
439
+ }>;
440
+ quote(symbol: string, options?: CallOptions): Promise<{
441
+ symbol: string;
442
+ price: number;
443
+ date: string;
444
+ }>;
445
+ quotes(body: {
446
+ optionSymbols: string[];
447
+ includeGreeks?: boolean;
448
+ }, options?: CallOptions): Promise<{
449
+ quotes: OptionQuote[];
450
+ }>;
451
+ earnings(symbol: string, options?: CallOptions): Promise<Earnings>;
452
+ earningsBatch(body: {
453
+ symbols: string[];
454
+ }, options?: CallOptions): Promise<{
455
+ earnings: Record<string, Earnings | null>;
456
+ }>;
457
+ fomc(params?: {
458
+ daysAhead?: number;
459
+ }, options?: CallOptions): Promise<{
460
+ upcoming: {
461
+ date: string;
462
+ daysUntil: number;
463
+ }[];
464
+ next: {
465
+ date: string;
466
+ daysUntil: number;
467
+ } | null;
468
+ allDates: string[];
469
+ }>;
470
+ expirations(params: {
471
+ symbol: string;
472
+ type?: 'call' | 'put';
473
+ daysAhead?: number;
474
+ }, options?: CallOptions): Promise<{
475
+ symbol: string;
476
+ type: string;
477
+ expirations: {
478
+ date: string;
479
+ dte: number;
480
+ }[];
481
+ }>;
482
+ fundamentals(symbol: string, options?: CallOptions): Promise<WireObject & {
483
+ symbol: string;
484
+ available: boolean;
485
+ name?: string | null;
486
+ eps?: number | null;
487
+ peRatio?: number | null;
488
+ dividendYield?: number | null;
489
+ marketCap?: number | null;
490
+ high52w?: number | null;
491
+ low52w?: number | null;
492
+ }>;
493
+ priceProbability(params: {
494
+ symbol: string;
495
+ dteMin?: number;
496
+ dteMax?: number;
497
+ }, options?: CallOptions): Promise<{
498
+ symbol: string;
499
+ currentPrice: number;
500
+ historicalDataPoints: number;
501
+ distributions: {
502
+ dte: number;
503
+ date: string;
504
+ dataPoints: number;
505
+ historical: Record<'min' | 'p10' | 'p25' | 'median' | 'mean' | 'p75' | 'p90' | 'max' | 'stdDev', number>;
506
+ }[];
507
+ }>;
508
+ ivr(symbol: string, params?: {
509
+ currentIV?: number;
510
+ historyDays?: number;
511
+ }, options?: CallOptions): Promise<IVR>;
512
+ ivrSymbols(options?: CallOptions): Promise<{
513
+ symbols: string[];
514
+ count: number;
515
+ source: string;
516
+ }>;
517
+ };
518
+ advisor: {
519
+ notifications(params?: {
520
+ urgency?: string;
521
+ includeHold?: boolean;
522
+ }, options?: CallOptions): Promise<{
523
+ positions: AdvisorPosition[];
524
+ summary: WireObject & {
525
+ filtered: number;
526
+ total: number;
527
+ needsAction: number;
528
+ };
529
+ }>;
530
+ analyze(ticker: string, params?: {
531
+ includeRollCandidates?: boolean;
532
+ }, options?: CallOptions): Promise<{
533
+ position: Pair;
534
+ signal: Signal;
535
+ config: Record<string, number>;
536
+ rollCandidates?: RollCandidate[];
537
+ rollCandidatesError?: string;
538
+ }>;
539
+ rollCandidates(params: {
540
+ symbol: string;
541
+ type: OptionType;
542
+ currentStrike: number;
543
+ btcAsk?: number;
544
+ contracts?: number;
545
+ }, options?: CallOptions): Promise<{
546
+ symbol: string;
547
+ type: OptionType;
548
+ currentStrike: number;
549
+ costToClose: number;
550
+ candidates: RollCandidate[];
551
+ count: number;
552
+ }>;
553
+ config(options?: CallOptions): Promise<{
554
+ config: Record<string, number>;
555
+ description: Record<string, string>;
556
+ }>;
557
+ };
558
+ account: {
559
+ get(options?: CallOptions): Promise<{
560
+ buyingPower: string;
561
+ cash: string;
562
+ }>;
563
+ };
564
+ orders: {
565
+ list(options?: CallOptions): Promise<{
566
+ orders: BrokerOrder[];
567
+ }>;
568
+ placeEquity(body: EquityOrderInput, options?: CallOptions): Promise<BrokerOrder>;
569
+ cancel(id: string, options?: CallOptions): Promise<{
570
+ success: boolean;
571
+ message: string;
572
+ }>;
573
+ placeOptions(body: OptionsOrderInput, options?: CallOptions): Promise<OptionsOrderResult>;
574
+ roll(body: RollOrderInput, options?: CallOptions): Promise<OptionsOrderResult>;
575
+ };
576
+ shares: {
577
+ list(options?: CallOptions): Promise<ShareMetadata[]>;
578
+ create(body: FormData | {
579
+ page: 'portfolio' | 'positions';
580
+ data: WireObject;
581
+ }, options?: CallOptions): Promise<{
582
+ shareUrl: string;
583
+ id: string;
584
+ }>;
585
+ delete(id: string, options?: CallOptions): Promise<{
586
+ success: boolean;
587
+ }>;
588
+ publicSnapshot(url: string, options?: CallOptions): Promise<Snapshot>;
589
+ };
590
+ settingsAlpaca: {
591
+ get(options?: CallOptions): Promise<AlpacaConfig>;
592
+ save(body: AlpacaCredentials, options?: CallOptions): Promise<AlpacaConfig>;
593
+ test(body?: Partial<AlpacaCredentials>, options?: CallOptions): Promise<{
594
+ ok: boolean;
595
+ error?: string;
596
+ details?: string | {
597
+ accountId: string;
598
+ status: string;
599
+ currency: string;
600
+ tradingBlocked: boolean;
601
+ accountBlocked: boolean;
602
+ };
603
+ }>;
604
+ delete(options?: CallOptions): Promise<{
605
+ deleted: boolean;
606
+ }>;
607
+ };
608
+ invitations: {
609
+ status(options?: CallOptions): Promise<{
610
+ status: 'active' | 'pending';
611
+ email: string;
612
+ }>;
613
+ redeem(body: {
614
+ code: string;
615
+ }, options?: CallOptions): Promise<{
616
+ status: 'active' | 'pending';
617
+ }>;
618
+ };
619
+ keys: {
620
+ list(options?: CallOptions): Promise<KeyMetadata[]>;
621
+ create(body: KeyInput, options?: CallOptions): Promise<KeyMetadata & {
622
+ apiKey: string;
623
+ }>;
624
+ revoke(id: string, options?: CallOptions): Promise<{
625
+ success: boolean;
626
+ }>;
627
+ };
628
+ backtest: {
629
+ run(params: BacktestQuery, options?: CallOptions): Promise<{
630
+ summary: {
631
+ totalPL: number;
632
+ totalTrades: number;
633
+ winRate: number;
634
+ avgPLPerTrade: number;
635
+ totalReturn: number;
636
+ annualizedReturn: number;
637
+ avgCapitalAtRisk: number;
638
+ tradingDays: number;
639
+ daysElapsed: number;
640
+ };
641
+ trades: Array<WireObject & {
642
+ date: string;
643
+ action: string;
644
+ type: string;
645
+ strike: number | null;
646
+ expiration: string | null;
647
+ contracts: number;
648
+ fillPrice: number | null;
649
+ premium: number;
650
+ pl: number | null;
651
+ score: number | null;
652
+ signal: string | null;
653
+ dte: number | null;
654
+ volume: number;
655
+ stockPrice: number | null;
656
+ cumulative: number | null;
657
+ }>;
658
+ priceHistory: Array<{
659
+ date: string;
660
+ price: number;
661
+ }>;
662
+ }>;
663
+ };
664
+ }
package/index.js ADDED
@@ -0,0 +1,214 @@
1
+ /* global URL, URLSearchParams, FormData, Headers, AbortController, TextDecoder, Response, setTimeout, clearTimeout */
2
+ /** @import { RequestOptions } from './types.js' */
3
+
4
+ /** An HTTP or transport failure. Bodies, headers and credentials are never retained. */
5
+ export class WheeliumError extends Error {
6
+ /** @param {string} message @param {{status?: number, code?: string, requestId?: string, requiredScopes?: string[]}} [details] */
7
+ constructor (message, details = {}) {
8
+ super(message)
9
+ this.name = 'WheeliumError'
10
+ this.status = details.status ?? 0
11
+ this.code = details.code ?? 'http_error'
12
+ this.requestId = details.requestId
13
+ this.requiredScopes = details.requiredScopes ?? []
14
+ }
15
+ }
16
+
17
+ function safeURL (value) {
18
+ let url
19
+ try { url = new URL(value) } catch { throw new WheeliumError('Invalid base or request URL', { code: 'unsafe_url' }) }
20
+ if ((url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) || url.username || url.password || url.hash) throw new WheeliumError('Unsafe URL: HTTPS without credentials or fragment required', { code: 'unsafe_url' })
21
+ return url
22
+ }
23
+ function redact (value, token, secrets = []) {
24
+ let text = String(value)
25
+ for (const secret of [token, ...secrets]) if (typeof secret === 'string' && secret) text = text.split(secret).join('[redacted]')
26
+ return text.replace(/Bearer\s+[^\s"']+/gi, 'Bearer [redacted]').replace(/whl_[A-Za-z0-9_-]+/g, '[redacted]').replace(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[redacted]')
27
+ }
28
+ function query (path, params = {}) {
29
+ const qs = new URLSearchParams()
30
+ for (const [key, value] of Object.entries(params)) if (value !== undefined && value !== null) qs.set(key, String(value))
31
+ return path + (qs.size ? `?${qs}` : '')
32
+ }
33
+ const id = value => encodeURIComponent(value)
34
+ const safeReads = new Set([
35
+ '/api/me', '/api/trades', '/api/options/contracts', '/api/options/quote', '/api/options/earnings',
36
+ '/api/options/fomc', '/api/options/expirations', '/api/options/fundamentals', '/api/options/price-probability',
37
+ '/api/wheel/notifications', '/api/wheel/roll-candidates', '/api/wheel/config', '/api/wheel/ivr',
38
+ '/api/settings/alpaca', '/api/settings/alpaca/account', '/api/orders/equity', '/api/share', '/api/invite/status', '/api/keys'
39
+ ])
40
+ function isSafeRead (path, method, authenticated) {
41
+ if (!authenticated) return method === 'GET'
42
+ const pathname = path.split('?')[0]
43
+ if (method === 'POST') return ['/api/options/quotes', '/api/options/earnings/batch'].includes(pathname)
44
+ return method === 'GET' && (safeReads.has(pathname) || /^\/api\/trades\/[^/]+$/.test(pathname) || /^\/api\/wheel\/ivr\/[^/]+$/.test(pathname) || /^\/api\/wheel\/position\/[^/]+\/analysis$/.test(pathname))
45
+ }
46
+ function abortable (promise, signal) {
47
+ return new Promise((resolve, reject) => {
48
+ const abort = () => reject(new Error('aborted'))
49
+ const settle = (callback, value) => { signal.removeEventListener('abort', abort); callback(value) }
50
+ Promise.resolve(promise).then(value => settle(resolve, value), error => settle(reject, error))
51
+ if (signal.aborted) abort()
52
+ else signal.addEventListener('abort', abort, { once: true })
53
+ })
54
+ }
55
+ async function readBody (response, signal) {
56
+ if (!response.body?.getReader) return abortable(typeof response.text === 'function' ? response.text() : response.json().then(JSON.stringify), signal)
57
+ const reader = response.body.getReader()
58
+ const decoder = new TextDecoder()
59
+ let text = ''
60
+ try {
61
+ while (true) {
62
+ const { value, done } = await abortable(reader.read(), signal)
63
+ if (done) return text + decoder.decode()
64
+ text += decoder.decode(value, { stream: true })
65
+ }
66
+ } catch (error) {
67
+ // Cancel explicitly even when an injected fetch ignores AbortSignal.
68
+ void reader.cancel().catch(() => {})
69
+ throw error
70
+ } finally { reader.releaseLock() }
71
+ }
72
+ function bodySecrets (body) {
73
+ if (typeof body !== 'string') return []
74
+ try {
75
+ const data = JSON.parse(body)
76
+ return Object.entries(data).filter(([key]) => /^(apiKey|keyId|secretKey|password|token|authToken)$/i.test(key)).map(([, value]) => value).filter(value => typeof value === 'string')
77
+ } catch { return [] }
78
+ }
79
+
80
+ /**
81
+ * Exactly one credential source; token providers are evaluated for each attempt.
82
+ * Only domain methods explicitly marked safe may retry (never mutations/backtests).
83
+ * @param {ClientOptions} options
84
+ * @returns {WheeliumClient}
85
+ */
86
+ export function createClient (options) {
87
+ if (!options || (options.apiKey !== undefined) === (options.getToken !== undefined) || (options.apiKey !== undefined && (typeof options.apiKey !== 'string' || !options.apiKey)) || (options.getToken !== undefined && typeof options.getToken !== 'function')) throw new TypeError('Exactly one credential source (apiKey or getToken) is required')
88
+ const base = safeURL(options.baseUrl ?? 'https://api.wheelium.app')
89
+ if (base.search) throw new WheeliumError('Invalid base URL query', { code: 'unsafe_url' })
90
+ const basePath = base.pathname.replace(/\/$/, '')
91
+ const timeoutMs = options.timeoutMs ?? 30000
92
+ const retries = options.maxRetries ?? 2
93
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isInteger(retries) || retries < 0 || retries > 3 || !Number.isFinite(options.retryDelayMs ?? 250) || (options.retryDelayMs ?? 250) < 0) throw new TypeError('Invalid timeout or retry limit')
94
+ /** @param {string} path @param {RequestOptions} [opts] */
95
+ async function request (path, opts = {}) {
96
+ const authenticated = opts.authenticated !== false
97
+ const method = (opts.method ?? 'GET').toUpperCase()
98
+ const retrySafe = opts.safeRetry === true && isSafeRead(path, method, authenticated)
99
+ let url
100
+ if (authenticated) {
101
+ // Relative paths must remain in the configured API base; no origin/path escape.
102
+ if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//') || /[\\\r\n]/.test(path) || /(?:^|\/)(?:\.|%2e){1,2}(?:\/|\?|$)/i.test(path)) throw new WheeliumError('Unsafe authenticated request URL', { code: 'unsafe_url' })
103
+ url = safeURL(`${base.origin}${basePath}${path}`)
104
+ if (url.origin !== base.origin || (basePath && !url.pathname.startsWith(`${basePath}/`))) throw new WheeliumError('Unsafe authenticated request URL', { code: 'unsafe_url' })
105
+ } else url = safeURL(path)
106
+ const controller = new AbortController()
107
+ let timedOut = false
108
+ const onAbort = () => controller.abort(opts.signal?.reason)
109
+ if (opts.signal?.aborted) onAbort()
110
+ else opts.signal?.addEventListener('abort', onAbort, { once: true })
111
+ const timer = setTimeout(() => { timedOut = true; controller.abort() }, timeoutMs)
112
+ const delay = ms => new Promise((resolve, reject) => {
113
+ if (controller.signal.aborted) return reject(new Error('aborted'))
114
+ const abort = () => { clearTimeout(handle); reject(new Error('aborted')) }
115
+ const handle = setTimeout(() => { controller.signal.removeEventListener('abort', abort); resolve() }, ms)
116
+ controller.signal.addEventListener('abort', abort, { once: true })
117
+ })
118
+ const retryDelay = (attempt, response) => {
119
+ const retryAfter = response?.headers?.get('Retry-After')
120
+ const retryMs = retryAfter == null ? null : (/^\d+(\.\d+)?$/.test(retryAfter) ? Number(retryAfter) * 1000 : Math.max(0, Date.parse(retryAfter) - Date.now()))
121
+ return delay(Number.isFinite(retryMs) && retryMs !== null ? retryMs + Math.random() * (options.retryDelayMs ?? 250) : (options.retryDelayMs ?? 250) * 2 ** attempt * (0.5 + Math.random()))
122
+ }
123
+ let token
124
+ try {
125
+ for (let attempt = 0; ; attempt++) {
126
+ if (controller.signal.aborted) throw new Error('aborted')
127
+ token = authenticated ? (options.getToken ? await abortable(Promise.resolve().then(options.getToken), controller.signal) : options.apiKey) : null
128
+ if (controller.signal.aborted) throw new Error('aborted')
129
+ const headers = new Headers(opts.headers)
130
+ headers.delete('Authorization')
131
+ headers.delete('Cookie')
132
+ const multipart = typeof FormData !== 'undefined' && opts.body instanceof FormData
133
+ if (multipart) headers.delete('Content-Type')
134
+ else if (opts.body !== undefined && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
135
+ if (token) headers.set('Authorization', `Bearer ${token}`)
136
+ let response
137
+ let text
138
+ try {
139
+ response = await abortable((options.fetch ?? globalThis.fetch)(url.href, { method, body: opts.body, headers, signal: controller.signal, redirect: 'manual', credentials: 'omit', ...(authenticated ? { cache: 'no-store' } : {}) }), controller.signal)
140
+ if (response.type === 'opaqueredirect' || response.redirected || (response.status >= 300 && response.status < 400)) throw new WheeliumError('Redirect rejected; credentials were not forwarded', { code: 'unsafe_redirect', status: response.status })
141
+ text = response.status === 204 ? '' : await readBody(response, controller.signal)
142
+ } catch (err) {
143
+ if (controller.signal.aborted || err instanceof WheeliumError) throw err
144
+ // Body transport failures use the same attempt/time budget as fetch.
145
+ // Known HTTP denials must not become retriable network errors.
146
+ if (retrySafe && attempt < retries && (!response || response.ok || [408, 429, 500, 502, 503, 504].includes(response.status))) { await retryDelay(attempt, response); continue }
147
+ if (!response || response.ok) throw err
148
+ // An unreadable error body still retains status and correlation ID;
149
+ // never retain partial bodies or transport diagnostics/credentials.
150
+ text = ''
151
+ }
152
+ const secrets = bodySecrets(opts.body)
153
+ if (controller.signal.aborted) throw new Error('aborted')
154
+ if (!response.ok && retrySafe && attempt < retries && [408, 429, 500, 502, 503, 504].includes(response.status)) {
155
+ await retryDelay(attempt, response)
156
+ continue
157
+ }
158
+ let data
159
+ try { data = text.trim() ? JSON.parse(text) : undefined } catch {
160
+ if (response.ok && !opts.rawResponse) throw new WheeliumError('Response was not valid JSON', { code: 'invalid_response', status: response.status })
161
+ }
162
+ if (!response.ok) throw new WheeliumError(redact(data?.error ?? data?.message ?? `HTTP error! status: ${response.status}`, token, secrets), {
163
+ status: response.status,
164
+ code: redact(typeof data?.code === 'string' ? data.code : 'http_error', token, secrets),
165
+ requestId: typeof (data?.requestId ?? response.headers?.get('X-Request-Id')) === 'string' ? redact(data?.requestId ?? response.headers?.get('X-Request-Id'), token, secrets) : undefined,
166
+ requiredScopes: Array.isArray(data?.requiredScopes) ? data.requiredScopes.filter(v => typeof v === 'string').map(v => redact(v, token, secrets)) : []
167
+ })
168
+ // Buffered responses retain legacy apiCall behavior, with body covered by timeout.
169
+ return opts.rawResponse ? new Response(response.status === 204 ? null : text, { status: response.status, headers: response.headers }) : data
170
+ }
171
+ } catch (error) {
172
+ if (controller.signal.aborted) throw new WheeliumError(timedOut ? 'Request timed out' : 'Request aborted', { code: timedOut ? 'timeout' : 'aborted' })
173
+ if (error instanceof WheeliumError) throw error
174
+ throw new WheeliumError('Network request failed', { code: 'network_error' })
175
+ } finally {
176
+ clearTimeout(timer)
177
+ opts.signal?.removeEventListener('abort', onAbort)
178
+ }
179
+ }
180
+ const read = (path, params, opts) => request(query(path, params), { ...opts, safeRetry: true })
181
+ const write = (path, method, body, opts) => request(path, { ...opts, method, body: body === undefined ? undefined : (typeof FormData !== 'undefined' && body instanceof FormData ? body : JSON.stringify(body)) })
182
+ return {
183
+ request,
184
+ me: opts => read('/api/me', {}, opts),
185
+ trades: {
186
+ list: opts => read('/api/trades', {}, opts), get: (tradeId, opts) => read(`/api/trades/${id(tradeId)}`, {}, opts),
187
+ pairs: (params, opts) => read('/api/trades', { ...params, pairs: true }, opts),
188
+ create: (body, opts) => write('/api/trades', 'POST', body, opts), update: (tradeId, body, opts) => write(`/api/trades/${id(tradeId)}`, 'PUT', body, opts),
189
+ delete: (tradeId, opts) => write(`/api/trades/${id(tradeId)}`, 'DELETE', undefined, opts), cancelBrokerOrder: (orderId, opts) => write(`/api/trades/${id(orderId)}/cancel`, 'POST', undefined, opts)
190
+ },
191
+ portfolio: { get: (params, opts) => read('/api/trades', { ...params, portfolio: true }, opts), dailyPL: (params, opts) => read('/api/trades', { ...params, dailyPL: true }, opts) },
192
+ market: {
193
+ contracts: (params, opts) => read('/api/options/contracts', params, opts), quote: (symbol, opts) => read('/api/options/quote', { symbol }, opts),
194
+ quotes: (body, opts) => request('/api/options/quotes', { ...opts, method: 'POST', body: JSON.stringify(body), safeRetry: true }),
195
+ earnings: (symbol, opts) => read('/api/options/earnings', { symbol }, opts), earningsBatch: (body, opts) => request('/api/options/earnings/batch', { ...opts, method: 'POST', body: JSON.stringify(body), safeRetry: true }),
196
+ fomc: (params, opts) => read('/api/options/fomc', params, opts), expirations: (params, opts) => read('/api/options/expirations', params, opts), fundamentals: (symbol, opts) => read('/api/options/fundamentals', { symbol }, opts),
197
+ priceProbability: (params, opts) => read('/api/options/price-probability', params, opts), ivr: (symbol, params, opts) => read(`/api/wheel/ivr/${id(symbol)}`, params, opts), ivrSymbols: opts => read('/api/wheel/ivr', {}, opts)
198
+ },
199
+ advisor: { notifications: (params, opts) => read('/api/wheel/notifications', params, opts), analyze: (ticker, params, opts) => read(`/api/wheel/position/${id(ticker)}/analysis`, params, opts), rollCandidates: (params, opts) => read('/api/wheel/roll-candidates', params, opts), config: opts => read('/api/wheel/config', {}, opts) },
200
+ account: { get: opts => read('/api/settings/alpaca/account', {}, opts) },
201
+ orders: { list: opts => read('/api/orders/equity', {}, opts), placeEquity: (body, opts) => write('/api/orders/equity', 'POST', body, opts), cancel: (orderId, opts) => write(`/api/orders/equity/${id(orderId)}/cancel`, 'POST', undefined, opts), placeOptions: (body, opts) => write('/api/options/orders', 'POST', body, opts), roll: (body, opts) => write('/api/options/orders/roll', 'POST', body, opts) },
202
+ shares: { list: opts => read('/api/share', {}, opts), create: (body, opts) => write('/api/share', 'POST', body, opts), delete: (shareId, opts) => write(`/api/share/${id(shareId)}`, 'DELETE', undefined, opts), publicSnapshot: (url, opts) => request(url, { ...opts, authenticated: false, safeRetry: true }) },
203
+ settingsAlpaca: { get: opts => read('/api/settings/alpaca', {}, opts), save: (body, opts) => write('/api/settings/alpaca', 'POST', body, opts), test: (body, opts) => write('/api/settings/alpaca/test', 'POST', body, opts), delete: opts => write('/api/settings/alpaca', 'DELETE', undefined, opts) },
204
+ invitations: { status: opts => read('/api/invite/status', {}, opts), redeem: (body, opts) => write('/api/invite/redeem', 'POST', body, opts) },
205
+ keys: { list: opts => read('/api/keys', {}, opts), create: (body, opts) => write('/api/keys', 'POST', body, opts), revoke: (keyId, opts) => write(`/api/keys/${id(keyId)}`, 'DELETE', undefined, opts) },
206
+ backtest: { run: (params, opts) => request(query('/api/backtest', params), opts) }
207
+ }
208
+ }
209
+
210
+ /** @typedef {import('./types.js').ClientOptions} ClientOptions */
211
+ /** @typedef {import('./types.js').WheeliumClient} WheeliumClient */
212
+ /** @typedef {import('./types.js').Trade} Trade */
213
+ /** @typedef {import('./types.js').Scope} Scope */
214
+ /** @typedef {import('./types.js').CallOptions} CallOptions */
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@wheelium/client",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Wheelium browser and Node fetch client",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./index.js"
13
+ }
14
+ },
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "index.js",
18
+ "dist/*.d.ts",
19
+ "README.md",
20
+ "LICENSE",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "license": "MIT",
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "registry": "https://registry.npmjs.org/"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/davidguttman/trade-tracker.git",
31
+ "directory": "packages/client"
32
+ }
33
+ }