@startsimpli/api 0.5.19 → 0.5.20

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/src/index.ts CHANGED
@@ -163,6 +163,7 @@ import { EnrichmentApi } from './lib/enrichment-api';
163
163
  import { TargetListsApi } from './lib/target-lists-api';
164
164
  import { MessageTemplatesApi } from './lib/message-templates-api';
165
165
  import { MarketsApi } from './lib/markets-api';
166
+ import { VaultApi } from './lib/vault-api';
166
167
 
167
168
  import type { ApiClientConfig } from './lib/api-client';
168
169
 
@@ -187,9 +188,25 @@ export function createStartSimpliApi(config: ApiClientConfig = {}) {
187
188
  targetLists: new TargetListsApi(client),
188
189
  messageTemplates: new MessageTemplatesApi(client),
189
190
  markets: new MarketsApi(client),
191
+ vault: new VaultApi(client),
190
192
  };
191
193
  }
192
194
 
195
+ // Vault API
196
+ export { VaultApi } from './lib/vault-api';
197
+ export type {
198
+ VaultEnvironment,
199
+ VaultEnvironmentInput,
200
+ VaultSecret,
201
+ VaultSecretInput,
202
+ VaultSecretReveal,
203
+ VaultAccessKey,
204
+ VaultAccessKeyCreated,
205
+ VaultAccessKeyInput,
206
+ VaultAuditEntry,
207
+ VaultListParams,
208
+ } from './lib/vault-api';
209
+
193
210
  // Markets API
194
211
  export { MarketsApi } from './lib/markets-api';
195
212
  export type {
@@ -239,9 +256,29 @@ export type {
239
256
  OptionsIvResponse,
240
257
  OptionsIvHistoryParams,
241
258
  OptionsSkewPoint,
259
+ OptionsChainRaw,
242
260
  OptionsGreeks,
261
+ OptionsUnusualActivityPoint,
262
+ OptionsUnusualActivityParams,
263
+ OptionsUnusualActivityResponse,
264
+ AtmIvHistoryPoint,
265
+ AtmIvHistoryParams,
266
+ AtmIvHistoryResponse,
267
+ MacroCategory,
268
+ MacroCalendarRow,
269
+ MacroCalendarParams,
270
+ MacroCalendarResponse,
243
271
  VixTenor,
244
272
  VixTermState,
245
273
  VixTermPoint,
246
274
  VixTermResponse,
275
+ BarsBulkParams,
276
+ BarsBulkResponse,
277
+ SocialPlatform,
278
+ SocialDirection,
279
+ SocialPost,
280
+ SocialPostMatchedEntity,
281
+ SocialPostResolvedContact,
282
+ SocialPostsParams,
283
+ SocialPostsResponse,
247
284
  } from './lib/markets-api';
@@ -9,23 +9,34 @@ export class ApiException extends Error {
9
9
  public statusText?: string;
10
10
  public errors?: Record<string, string[]>;
11
11
  public detail?: string;
12
-
13
- constructor(message: string, options?: Partial<DRFApiError>) {
12
+ /** Machine-readable code from the standardized error response (e.g.
13
+ * 'limit_reached', 'no_company'). Set by parseErrorResponse when the
14
+ * backend emits a structured error shape via the core exception handler. */
15
+ public code?: string;
16
+ /** Extra context the backend stapled onto the standardized response —
17
+ * e.g. limit_reached carries { feature_key, limit, current }. */
18
+ public details?: Record<string, unknown>;
19
+
20
+ constructor(message: string, options?: Partial<DRFApiError> & { code?: string; details?: Record<string, unknown> }) {
14
21
  super(message);
15
22
  this.name = 'ApiException';
16
23
  this.status = options?.status;
17
24
  this.statusText = options?.statusText;
18
25
  this.errors = options?.errors;
19
26
  this.detail = options?.detail;
27
+ this.code = options?.code;
28
+ this.details = options?.details;
20
29
  }
21
30
 
22
- toJSON(): DRFApiError {
31
+ toJSON(): DRFApiError & { code?: string; details?: Record<string, unknown> } {
23
32
  return {
24
33
  message: this.message,
25
34
  detail: this.detail,
26
35
  status: this.status,
27
36
  statusText: this.statusText,
28
37
  errors: this.errors,
38
+ code: this.code,
39
+ details: this.details,
29
40
  };
30
41
  }
31
42
  }
@@ -41,7 +52,31 @@ export async function parseErrorResponse(response: Response): Promise<DRFApiErro
41
52
  try {
42
53
  const data = await response.json();
43
54
 
44
- // Django REST Framework error format
55
+ // 1) Standardized response shape from apps.core.exceptions:
56
+ // { error, code, statusCode, fieldErrors?, ...details } — the
57
+ // 'error' field carries the human message; 'code' is the machine
58
+ // code (e.g. 'limit_reached'); any extra keys are structured
59
+ // context the view stapled on (feature_key, limit, current, …).
60
+ // Snake_case keys keep through camelCase transform too (already
61
+ // converted by snakeToCamel before we get here when the client
62
+ // applies it; both shapes handled here for safety).
63
+ if (typeof data === 'object' && data !== null && typeof (data.error ?? data.message) === 'string' && data.code) {
64
+ const reservedKeys = new Set(['error', 'code', 'statusCode', 'status_code', 'fieldErrors', 'field_errors', 'timestamp', 'requestId', 'request_id', 'detail']);
65
+ const extras: Record<string, unknown> = {};
66
+ for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
67
+ if (!reservedKeys.has(k)) extras[k] = v;
68
+ }
69
+ return {
70
+ message: (data.error as string) ?? (data.message as string),
71
+ detail: (data.detail as string | undefined) ?? (data.error as string),
72
+ code: data.code as string,
73
+ details: Object.keys(extras).length > 0 ? extras : undefined,
74
+ status: response.status,
75
+ statusText: response.statusText,
76
+ };
77
+ }
78
+
79
+ // 2) Plain Django REST Framework error: { detail: '...' }
45
80
  if (data.detail) {
46
81
  return {
47
82
  detail: data.detail,
@@ -51,7 +86,7 @@ export async function parseErrorResponse(response: Response): Promise<DRFApiErro
51
86
  };
52
87
  }
53
88
 
54
- // Validation errors format
89
+ // 3) Field-level validation errors: { email: ['...'], password: ['...'] }
55
90
  if (typeof data === 'object') {
56
91
  return {
57
92
  errors: data,
package/src/lib/errors.ts CHANGED
@@ -70,8 +70,13 @@ export class AppError extends Error {
70
70
  this.details = details;
71
71
  this.isOperational = isOperational;
72
72
 
73
- // Maintains proper stack trace for where our error was thrown
74
- Error.captureStackTrace(this, this.constructor);
73
+ // Maintains proper stack trace where supported (V8/Hermes). Guarded and
74
+ // typed locally so the package stays portable to engines/tsconfigs without
75
+ // the Node-only Error.captureStackTrace global (e.g. React Native).
76
+ const captureStackTrace = (
77
+ Error as { captureStackTrace?: (target: object, ctor?: unknown) => void }
78
+ ).captureStackTrace;
79
+ captureStackTrace?.(this, this.constructor);
75
80
  }
76
81
 
77
82
  /**
@@ -257,6 +257,18 @@ export interface OptionContract {
257
257
  volume: number;
258
258
  }
259
259
 
260
+ // Server returns separate calls[]/puts[] (confirmed stable, mac req 22ac4889).
261
+ // Client flattens into a single contracts[] with side tag.
262
+ export interface OptionsChainRaw {
263
+ symbol: string;
264
+ expiry: string;
265
+ spot: number | string;
266
+ calls?: Omit<OptionContract, 'side'>[];
267
+ puts?: Omit<OptionContract, 'side'>[];
268
+ expiries?: string[];
269
+ computedAt?: string | null;
270
+ }
271
+
260
272
  export interface OptionsChainResponse {
261
273
  symbol: string;
262
274
  expiry: string;
@@ -271,7 +283,8 @@ export interface OptionsChainParams {
271
283
  expiry?: string;
272
284
  }
273
285
 
274
- // Per brain-trading req b09a6bb6 — atm/skew/put-call ratios as a single snapshot row
286
+ // Per brain-trading req b09a6bb6 — atm/skew/put-call ratios as a single snapshot row.
287
+ // realized_vol_30d + iv_rv_spread_30d added in mac req 22ac4889.
275
288
  export interface OptionsIvPoint {
276
289
  snapshotDate: string;
277
290
  atmIv: number;
@@ -284,6 +297,11 @@ export interface OptionsIvPoint {
284
297
  // Populated once 20+ days of history accumulate
285
298
  ivRank30d: number | null;
286
299
  ivRank252d: number | null;
300
+ atmIvPercentile252d: number | null;
301
+ // Annualized 30d stdev of close-to-close log returns (decimal, e.g. 0.42)
302
+ realizedVol30d: number | null;
303
+ // atm_iv - realized_vol_30d. >0 ⇒ options priced rich vs realized.
304
+ ivRvSpread30d: number | null;
287
305
  }
288
306
 
289
307
  export interface OptionsIvResponse {
@@ -298,13 +316,68 @@ export interface OptionsIvHistoryParams {
298
316
  }
299
317
 
300
318
  // Skew is derived client-side from chain (per brain-trading guidance);
301
- // kept as a local UI type, not an endpoint.
319
+ // kept as a local UI type for the chart.
302
320
  export interface OptionsSkewPoint {
303
321
  strike: number;
304
322
  iv: number;
305
323
  side: OptionSide;
306
324
  }
307
325
 
326
+ export interface BarsBulkParams {
327
+ symbols: string[];
328
+ interval?: BarInterval;
329
+ since?: string;
330
+ until?: string;
331
+ }
332
+
333
+ export interface BarsBulkResponse {
334
+ interval: BarInterval;
335
+ count: number;
336
+ results: Record<string, PriceBar[]>;
337
+ }
338
+
339
+ export type SocialPlatform = 'truth_social' | string;
340
+ export type SocialDirection = 'bullish' | 'bearish' | 'neutral' | 'unclear';
341
+
342
+ export interface SocialPostMatchedEntity {
343
+ symbol: string;
344
+ name?: string | null;
345
+ }
346
+
347
+ export interface SocialPostResolvedContact {
348
+ id: string;
349
+ name: string;
350
+ }
351
+
352
+ export interface SocialPost {
353
+ id: string;
354
+ platform: SocialPlatform;
355
+ postedAt: string;
356
+ url?: string | null;
357
+ text?: string | null;
358
+ resolvedContact: SocialPostResolvedContact | null;
359
+ matchedEntities: SocialPostMatchedEntity[];
360
+ sentiment: number | null;
361
+ sentimentLabel: NewsSentimentLabel | null;
362
+ eventType: string | null;
363
+ marketMovingScore: number | null;
364
+ directionForSymbol: SocialDirection | null;
365
+ }
366
+
367
+ export interface SocialPostsParams {
368
+ symbol?: string;
369
+ platform?: SocialPlatform;
370
+ since?: string;
371
+ until?: string;
372
+ direction?: SocialDirection;
373
+ limit?: number;
374
+ }
375
+
376
+ export interface SocialPostsResponse {
377
+ count: number;
378
+ results: SocialPost[];
379
+ }
380
+
308
381
  export interface OptionsGreeks {
309
382
  symbol: string;
310
383
  expiry: string;
@@ -318,8 +391,85 @@ export interface OptionsGreeks {
318
391
  computedAt: string | null;
319
392
  }
320
393
 
394
+ // ATM IV history derived from Tradier OptionContractBar via BS inversion (mac req e5b8c299).
395
+ // Deeper history than /options/iv/history/ for symbols where contract listings predate
396
+ // MarketOptionsSnapshot accumulation (UCO has 112 rows back to Sep 2025).
397
+ export interface AtmIvHistoryPoint {
398
+ date: string;
399
+ spot: number;
400
+ atmStrike: number;
401
+ atmCallIv: number | null;
402
+ atmPutIv: number | null;
403
+ atmIv: number | null;
404
+ ivRank30d: number | null;
405
+ ivRank252d: number | null;
406
+ }
407
+
408
+ export interface AtmIvHistoryParams {
409
+ symbol: string;
410
+ since?: string;
411
+ until?: string;
412
+ // Drop rows where both call AND put closest-strike fall outside |strike/spot - 1| > moneynessMax.
413
+ // Omit = full series. Recommended 0.10 to filter early UCO Sep-2025 garbage; tighter (0.05) risks
414
+ // dropping thin-coverage days entirely. (mac req 47ac5ec6)
415
+ moneynessMax?: number;
416
+ }
417
+
418
+ export interface AtmIvHistoryResponse {
419
+ symbol: string;
420
+ count: number;
421
+ results: AtmIvHistoryPoint[];
422
+ }
423
+
424
+ // Macro event calendar (mac req e5b8c299). FOMC + CPI + NFP + PCE + EIA petroleum/natgas.
425
+ export type MacroCategory = 'fomc' | 'cpi' | 'nfp' | 'pce' | 'eia_petroleum' | 'eia_natgas' | string;
426
+
427
+ export interface MacroCalendarRow {
428
+ releaseAt: string;
429
+ category: MacroCategory;
430
+ name: string;
431
+ expectedMovers: string[];
432
+ }
433
+
434
+ export interface MacroCalendarParams {
435
+ since?: string;
436
+ until?: string;
437
+ categories?: MacroCategory[];
438
+ movers?: string[];
439
+ }
440
+
441
+ export interface MacroCalendarResponse {
442
+ count: number;
443
+ results: MacroCalendarRow[];
444
+ }
445
+
446
+ // Unusual options activity z-score per underlying (mac req 748b457e).
447
+ // z_score = (latest_volume - mean) / std over the trailing lookback window.
448
+ // sample_size < 5 ⇒ "collecting"; z_score >= 2 ⇒ unusual.
449
+ export interface OptionsUnusualActivityPoint {
450
+ symbol: string;
451
+ latestDate: string | null;
452
+ latestVolume: number | null;
453
+ mean: number | null;
454
+ std: number | null;
455
+ zScore: number | null;
456
+ sampleSize: number;
457
+ lookback: number;
458
+ }
459
+
460
+ export interface OptionsUnusualActivityParams {
461
+ symbols: string[];
462
+ lookback?: number;
463
+ }
464
+
465
+ export interface OptionsUnusualActivityResponse {
466
+ count: number;
467
+ results: OptionsUnusualActivityPoint[];
468
+ }
469
+
321
470
  export type VixTenor = '9D' | '30D' | '3M' | string;
322
- export type VixTermState = 'contango' | 'backwardation';
471
+ // 3-state regime confirmed by mac req 22ac4889 — threshold ±2% spread VIX vs VIX3M.
472
+ export type VixTermState = 'contango_calm' | 'contango_neutral' | 'backwardation_stress';
323
473
 
324
474
  export interface VixTermPoint {
325
475
  tenor: VixTenor;
@@ -460,6 +610,9 @@ function coerceIvPoint(p: OptionsIvPoint): OptionsIvPoint {
460
610
  putCallOiRatio: maybeN(p.putCallOiRatio),
461
611
  ivRank30d: maybeN(p.ivRank30d),
462
612
  ivRank252d: maybeN(p.ivRank252d),
613
+ atmIvPercentile252d: maybeN(p.atmIvPercentile252d),
614
+ realizedVol30d: maybeN(p.realizedVol30d),
615
+ ivRvSpread30d: maybeN(p.ivRvSpread30d),
463
616
  };
464
617
  }
465
618
 
@@ -540,13 +693,19 @@ export class MarketsApi {
540
693
  // ===== Options + VIX (tentative — see agent_bridge req 42c763b2) =======
541
694
 
542
695
  async getOptionsChain(params: OptionsChainParams): Promise<OptionsChainResponse> {
543
- const res = await this.client.fetch.get<OptionsChainResponse>(ENDPOINTS.OPTIONS_CHAIN, {
544
- params: params as unknown as Record<string, unknown>,
696
+ const { symbol, expiry } = params;
697
+ const res = await this.client.fetch.get<OptionsChainRaw>(ENDPOINTS.OPTIONS_CHAIN(symbol), {
698
+ params: expiry ? { expiry } : undefined,
545
699
  });
700
+ const calls = (res.calls ?? []).map((c) => coerceContract({ ...c, side: 'call' as const }));
701
+ const puts = (res.puts ?? []).map((c) => coerceContract({ ...c, side: 'put' as const }));
546
702
  return {
547
- ...res,
703
+ symbol: res.symbol,
704
+ expiry: res.expiry,
548
705
  spot: n(res.spot),
549
- contracts: (res.contracts ?? []).map(coerceContract),
706
+ contracts: [...calls, ...puts],
707
+ expiries: res.expiries,
708
+ computedAt: res.computedAt,
550
709
  };
551
710
  }
552
711
 
@@ -573,6 +732,77 @@ export class MarketsApi {
573
732
  };
574
733
  }
575
734
 
735
+ async getAtmIvHistory(params: AtmIvHistoryParams): Promise<AtmIvHistoryResponse> {
736
+ const { symbol, moneynessMax, ...rest } = params;
737
+ const res = await this.client.fetch.get<AtmIvHistoryResponse>(ENDPOINTS.OPTIONS_ATM_IV_HISTORY(symbol), {
738
+ params: {
739
+ ...rest,
740
+ ...(moneynessMax != null ? { moneyness_max: moneynessMax } : {}),
741
+ } as Record<string, unknown>,
742
+ });
743
+ return {
744
+ ...res,
745
+ results: (res.results ?? []).map((p) => ({
746
+ ...p,
747
+ spot: n(p.spot),
748
+ atmStrike: n(p.atmStrike),
749
+ atmCallIv: maybeN(p.atmCallIv),
750
+ atmPutIv: maybeN(p.atmPutIv),
751
+ atmIv: maybeN(p.atmIv),
752
+ ivRank30d: maybeN(p.ivRank30d),
753
+ ivRank252d: maybeN(p.ivRank252d),
754
+ })),
755
+ };
756
+ }
757
+
758
+ async getMacroCalendar(params?: MacroCalendarParams): Promise<MacroCalendarResponse> {
759
+ const { categories, movers, ...rest } = params ?? {};
760
+ return this.client.fetch.get<MacroCalendarResponse>(ENDPOINTS.MACRO_CALENDAR, {
761
+ params: {
762
+ ...rest,
763
+ ...(categories ? { categories: categories.join(',') } : {}),
764
+ ...(movers ? { movers: movers.join(',') } : {}),
765
+ } as Record<string, unknown>,
766
+ });
767
+ }
768
+
769
+ async getOptionsUnusualActivity(params: OptionsUnusualActivityParams): Promise<OptionsUnusualActivityResponse> {
770
+ const { symbols, lookback } = params;
771
+ const res = await this.client.fetch.get<OptionsUnusualActivityResponse>(ENDPOINTS.OPTIONS_UNUSUAL_ACTIVITY, {
772
+ params: { symbols: symbols.join(','), ...(lookback != null ? { lookback } : {}) } as Record<string, unknown>,
773
+ });
774
+ return {
775
+ ...res,
776
+ results: (res.results ?? []).map((p) => ({
777
+ ...p,
778
+ latestVolume: maybeN(p.latestVolume),
779
+ mean: maybeN(p.mean),
780
+ std: maybeN(p.std),
781
+ zScore: maybeN(p.zScore),
782
+ sampleSize: n(p.sampleSize),
783
+ lookback: n(p.lookback),
784
+ })),
785
+ };
786
+ }
787
+
788
+ async getBarsBulk(params: BarsBulkParams): Promise<BarsBulkResponse> {
789
+ const { symbols, ...rest } = params;
790
+ const res = await this.client.fetch.get<BarsBulkResponse>(ENDPOINTS.BARS_BULK, {
791
+ params: { ...rest, symbols: symbols.join(','), format: 'json' } as Record<string, unknown>,
792
+ });
793
+ const out: Record<string, PriceBar[]> = {};
794
+ for (const [sym, bars] of Object.entries(res.results ?? {})) {
795
+ out[sym] = (bars ?? []).map(coerceBar);
796
+ }
797
+ return { ...res, results: out };
798
+ }
799
+
800
+ async getSocialPosts(params?: SocialPostsParams): Promise<SocialPostsResponse> {
801
+ return this.client.fetch.get<SocialPostsResponse>(ENDPOINTS.SOCIAL_POSTS, {
802
+ params: params as Record<string, unknown> | undefined,
803
+ });
804
+ }
805
+
576
806
  async getVixTerm(): Promise<VixTermResponse> {
577
807
  const res = await this.client.fetch.get<VixTermResponse>(ENDPOINTS.VIX_TERM);
578
808
  return { ...res, points: (res.points ?? []).map(coerceVixPoint) };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Platform-neutral sanitize helpers — no DOM, no DOMPurify.
3
+ *
4
+ * Shared by sanitize.ts (web/node, DOMPurify-backed `sanitizeHtml`) and
5
+ * sanitize.native.ts (React Native, DOM-free `sanitizeHtml`). Keeping these
6
+ * pure functions here avoids duplicating them across the two platform entries.
7
+ */
8
+
9
+ /**
10
+ * Validate and sanitize a search query string.
11
+ *
12
+ * Trims whitespace, escapes regex special characters, and limits length to 100 chars.
13
+ */
14
+ export function sanitizeSearchQuery(query: string): string {
15
+ if (!query || typeof query !== 'string') {
16
+ return '';
17
+ }
18
+
19
+ return query
20
+ .trim()
21
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
22
+ .substring(0, 100); // Limit length
23
+ }
24
+
25
+ /**
26
+ * Validate that a string is a safe SQL/API identifier.
27
+ *
28
+ * Only allows alphanumeric characters, underscores, and hyphens.
29
+ */
30
+ export function validateIdentifier(input: string): boolean {
31
+ return /^[a-zA-Z0-9_-]+$/.test(input);
32
+ }
@@ -0,0 +1,17 @@
1
+ export { sanitizeSearchQuery, validateIdentifier } from './sanitize-core';
2
+
3
+ /**
4
+ * XSS protection for React Native, where there is no DOM and no DOMPurify
5
+ * (isomorphic-dompurify pulls in jsdom, which cannot be bundled by Metro).
6
+ *
7
+ * React Native renders text through <Text>, never as interpreted HTML, so the
8
+ * safe and correct transform is to strip all markup and return plain text. The
9
+ * web build (sanitize.ts) keeps an allowlist via DOMPurify because the browser
10
+ * actually renders the result as HTML; RN never does.
11
+ */
12
+ export function sanitizeHtml(input: string): string {
13
+ if (!input || typeof input !== 'string') {
14
+ return '';
15
+ }
16
+ return input.replace(/<[^>]*>/g, '');
17
+ }
@@ -1,9 +1,14 @@
1
1
  import DOMPurify from 'isomorphic-dompurify';
2
2
 
3
+ export { sanitizeSearchQuery, validateIdentifier } from './sanitize-core';
4
+
3
5
  /**
4
- * XSS Protection - sanitize HTML content
6
+ * XSS Protection - sanitize HTML content (web/node, via DOMPurify).
5
7
  *
6
8
  * Strips all tags except a safe allowlist and removes dangerous attributes.
9
+ *
10
+ * React Native has no DOM, and bundling isomorphic-dompurify drags in jsdom,
11
+ * so Metro resolves the `sanitize.native.ts` sibling on native builds instead.
7
12
  */
8
13
  export function sanitizeHtml(input: string): string {
9
14
  return DOMPurify.sanitize(input, {
@@ -12,28 +17,3 @@ export function sanitizeHtml(input: string): string {
12
17
  ALLOW_DATA_ATTR: false,
13
18
  });
14
19
  }
15
-
16
- /**
17
- * Validate and sanitize a search query string.
18
- *
19
- * Trims whitespace, escapes regex special characters, and limits length to 100 chars.
20
- */
21
- export function sanitizeSearchQuery(query: string): string {
22
- if (!query || typeof query !== 'string') {
23
- return '';
24
- }
25
-
26
- return query
27
- .trim()
28
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
29
- .substring(0, 100); // Limit length
30
- }
31
-
32
- /**
33
- * Validate that a string is a safe SQL/API identifier.
34
- *
35
- * Only allows alphanumeric characters, underscores, and hyphens.
36
- */
37
- export function validateIdentifier(input: string): boolean {
38
- return /^[a-zA-Z0-9_-]+$/.test(input);
39
- }
@@ -0,0 +1,62 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { VaultApi } from './vault-api';
3
+
4
+ function makeApi() {
5
+ const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
6
+ // VaultApi only ever touches client.fetch.*
7
+ const api = new VaultApi({ fetch } as never);
8
+ return { api, fetch };
9
+ }
10
+
11
+ describe('VaultApi', () => {
12
+ let api: VaultApi;
13
+ let fetch: { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; patch: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
14
+
15
+ beforeEach(() => {
16
+ ({ api, fetch } = makeApi());
17
+ });
18
+
19
+ it('listEnvironments hits the environments endpoint with params', async () => {
20
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
21
+ await api.listEnvironments({ page: 2 });
22
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/vault/environments', { params: { page: 2 } });
23
+ });
24
+
25
+ it('createSecret posts to the env-scoped secrets endpoint', async () => {
26
+ fetch.post.mockResolvedValue({ id: '1', key: 'K' });
27
+ await api.createSecret('quinns-mac', { key: 'K', value: 'v' });
28
+ expect(fetch.post).toHaveBeenCalledWith(
29
+ 'api/v1/vault/environments/quinns-mac/secrets',
30
+ { key: 'K', value: 'v' },
31
+ );
32
+ });
33
+
34
+ it('revealSecret gets the reveal endpoint and returns the value', async () => {
35
+ fetch.get.mockResolvedValue({ key: 'K', value: 'sk-secret' });
36
+ const r = await api.revealSecret('quinns-mac', 'abc');
37
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/vault/environments/quinns-mac/secrets/abc/reveal');
38
+ expect(r.value).toBe('sk-secret');
39
+ });
40
+
41
+ it('createAccessKey returns the raw key once', async () => {
42
+ fetch.post.mockResolvedValue({ id: '1', name: 'laptop', key: 'RAW-KEY' });
43
+ const r = await api.createAccessKey('quinns-mac', { name: 'laptop' });
44
+ expect(fetch.post).toHaveBeenCalledWith(
45
+ 'api/v1/vault/environments/quinns-mac/access-keys',
46
+ { name: 'laptop' },
47
+ );
48
+ expect(r.key).toBe('RAW-KEY');
49
+ });
50
+
51
+ it('deleteEnvironment deletes by slug', async () => {
52
+ fetch.delete.mockResolvedValue(undefined);
53
+ await api.deleteEnvironment('prod');
54
+ expect(fetch.delete).toHaveBeenCalledWith('api/v1/vault/environments/prod');
55
+ });
56
+
57
+ it('listAudit hits the audit endpoint', async () => {
58
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
59
+ await api.listAudit('prod');
60
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/vault/environments/prod/audit', { params: undefined });
61
+ });
62
+ });