@startsimpli/api 0.5.19 → 0.5.21
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/README.md +287 -240
- package/package.json +20 -11
- package/src/constants/endpoints.ts +42 -2
- package/src/index.ts +53 -1
- package/src/lib/__tests__/companies-api.test.ts +53 -0
- package/src/lib/__tests__/domain-claims-api.test.ts +68 -0
- package/src/lib/__tests__/team-invitations-api.test.ts +50 -0
- package/src/lib/__tests__/teams-api.test.ts +74 -0
- package/src/lib/companies-api.ts +48 -0
- package/src/lib/domain-claims-api.ts +63 -0
- package/src/lib/error-handler.ts +40 -5
- package/src/lib/errors.ts +7 -2
- package/src/lib/markets-api.ts +237 -7
- package/src/lib/sanitize-core.ts +32 -0
- package/src/lib/sanitize.native.ts +17 -0
- package/src/lib/sanitize.ts +6 -26
- package/src/lib/team-invitations-api.ts +49 -0
- package/src/lib/teams-api.ts +73 -0
- package/src/lib/users-api.ts +13 -0
- package/src/lib/vault-api.test.ts +62 -0
- package/src/lib/vault-api.ts +155 -0
- package/src/types/api.ts +6 -0
- package/src/types/index.ts +22 -0
- package/src/types/team.ts +175 -0
- package/src/types/user.ts +20 -1
package/src/lib/markets-api.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
544
|
-
|
|
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
|
-
|
|
703
|
+
symbol: res.symbol,
|
|
704
|
+
expiry: res.expiry,
|
|
548
705
|
spot: n(res.spot),
|
|
549
|
-
contracts:
|
|
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
|
+
}
|
package/src/lib/sanitize.ts
CHANGED
|
@@ -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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeamInvitations API wrapper for /api/v1/team-invitations/*.
|
|
3
|
+
*
|
|
4
|
+
* The accept endpoint is intentionally public-ish — it accepts an unauth
|
|
5
|
+
* caller carrying the one-time invite token in the body. The TeamMember
|
|
6
|
+
* row is created idempotently by the backend manager (startsim-tsm).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ApiClient } from './api-client';
|
|
10
|
+
import { ENDPOINTS } from '../constants/endpoints';
|
|
11
|
+
import type {
|
|
12
|
+
TeamInvitation,
|
|
13
|
+
TeamInvitationListParams,
|
|
14
|
+
CreateTeamInvitationInput,
|
|
15
|
+
} from '../types/team';
|
|
16
|
+
import type { PaginatedResponse } from '../types';
|
|
17
|
+
|
|
18
|
+
export class TeamInvitationsApi {
|
|
19
|
+
constructor(private client: ApiClient) {}
|
|
20
|
+
|
|
21
|
+
list(params?: TeamInvitationListParams): Promise<PaginatedResponse<TeamInvitation>> {
|
|
22
|
+
return this.client.fetch.get<PaginatedResponse<TeamInvitation>>(
|
|
23
|
+
ENDPOINTS.TEAM_INVITATIONS,
|
|
24
|
+
{ params },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send a single invitation. The create response includes the raw token;
|
|
30
|
+
* subsequent reads will redact it.
|
|
31
|
+
*/
|
|
32
|
+
create(input: CreateTeamInvitationInput): Promise<TeamInvitation> {
|
|
33
|
+
return this.client.fetch.post<TeamInvitation>(ENDPOINTS.TEAM_INVITATIONS, input);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
revoke(id: string): Promise<void> {
|
|
37
|
+
return this.client.fetch.post<void>(ENDPOINTS.TEAM_INVITATION_REVOKE(id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Accept an invitation. The caller posts the one-time token; the backend
|
|
42
|
+
* wires the TeamMember idempotently (startsim-tsm).
|
|
43
|
+
*/
|
|
44
|
+
accept(id: string, token: string): Promise<TeamInvitation> {
|
|
45
|
+
return this.client.fetch.post<TeamInvitation>(ENDPOINTS.TEAM_INVITATION_ACCEPT(id), {
|
|
46
|
+
token,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teams API wrapper for /api/v1/teams/* plus the bulk-mgmt actions shipped
|
|
3
|
+
* by startsim-tsm (bulk-invite, remove-member, update-role).
|
|
4
|
+
*
|
|
5
|
+
* The Django TeamViewSet accepts numeric id OR slug. Member-mutation
|
|
6
|
+
* endpoints take user_id in the body, not the URL. startsim-o7s.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ApiClient } from './api-client';
|
|
10
|
+
import { ENDPOINTS } from '../constants/endpoints';
|
|
11
|
+
import type {
|
|
12
|
+
Team,
|
|
13
|
+
TeamMember,
|
|
14
|
+
TeamListParams,
|
|
15
|
+
BulkInviteEntry,
|
|
16
|
+
BulkInviteResult,
|
|
17
|
+
TeamRole,
|
|
18
|
+
} from '../types/team';
|
|
19
|
+
import type { PaginatedResponse } from '../types';
|
|
20
|
+
|
|
21
|
+
/** Shape returned by /team-members/my-teams/ — a flat array of memberships. */
|
|
22
|
+
export interface MyTeamMembership extends TeamMember {
|
|
23
|
+
team?: Team;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TeamsApi {
|
|
27
|
+
constructor(private client: ApiClient) {}
|
|
28
|
+
|
|
29
|
+
/** List teams visible to the current user. */
|
|
30
|
+
list(params?: TeamListParams): Promise<PaginatedResponse<Team>> {
|
|
31
|
+
return this.client.fetch.get<PaginatedResponse<Team>>(ENDPOINTS.TEAMS, { params });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Retrieve a single team by id or slug. */
|
|
35
|
+
retrieve(idOrSlug: string): Promise<Team> {
|
|
36
|
+
return this.client.fetch.get<Team>(ENDPOINTS.TEAM(idOrSlug));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** List members of a team. Backend returns the full set (no pagination). */
|
|
40
|
+
members(idOrSlug: string): Promise<TeamMember[]> {
|
|
41
|
+
return this.client.fetch.get<TeamMember[]>(ENDPOINTS.TEAM_MEMBERS(idOrSlug));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bulk-invite endpoint shipped by startsim-tsm. Sends one POST with the
|
|
46
|
+
* full list of {email, role} pairs and returns invited + skipped.
|
|
47
|
+
*/
|
|
48
|
+
bulkInvite(idOrSlug: string, invitations: BulkInviteEntry[]): Promise<BulkInviteResult> {
|
|
49
|
+
return this.client.fetch.post<BulkInviteResult>(ENDPOINTS.TEAM_BULK_INVITE(idOrSlug), {
|
|
50
|
+
invitations,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Remove a member from the team. user_id goes in the body (not the URL). */
|
|
55
|
+
removeMember(idOrSlug: string, userId: string): Promise<void> {
|
|
56
|
+
return this.client.fetch.post<void>(ENDPOINTS.TEAM_REMOVE_MEMBER(idOrSlug), {
|
|
57
|
+
userId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Promote/demote a member — fails if it would leave zero OWNERs. */
|
|
62
|
+
updateRole(idOrSlug: string, userId: string, role: TeamRole): Promise<TeamMember> {
|
|
63
|
+
return this.client.fetch.post<TeamMember>(ENDPOINTS.TEAM_UPDATE_ROLE(idOrSlug), {
|
|
64
|
+
userId,
|
|
65
|
+
role,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Return the current user's membership rows across every team they touch. */
|
|
70
|
+
myTeams(): Promise<MyTeamMembership[]> {
|
|
71
|
+
return this.client.fetch.get<MyTeamMembership[]>(ENDPOINTS.TEAM_MEMBERS_MY_TEAMS);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/lib/users-api.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
UpdateProfileRequest,
|
|
8
8
|
ChangePasswordRequest,
|
|
9
9
|
ChangePasswordResponse,
|
|
10
|
+
EarlyRegisterRequest,
|
|
11
|
+
EarlyRegisterResponse,
|
|
10
12
|
} from '../types/user';
|
|
11
13
|
import { ENDPOINTS } from '../constants/endpoints';
|
|
12
14
|
import type { ApiClient } from './api-client';
|
|
@@ -37,4 +39,15 @@ export class UsersApi {
|
|
|
37
39
|
data
|
|
38
40
|
);
|
|
39
41
|
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Passwordless early-access registration (name + email only).
|
|
45
|
+
* Public endpoint — no auth token required.
|
|
46
|
+
*/
|
|
47
|
+
async earlyRegister(data: EarlyRegisterRequest): Promise<EarlyRegisterResponse> {
|
|
48
|
+
return this.client.fetch.post<EarlyRegisterResponse>(
|
|
49
|
+
ENDPOINTS.AUTH_EARLY_REGISTER,
|
|
50
|
+
data
|
|
51
|
+
);
|
|
52
|
+
}
|
|
40
53
|
}
|
|
@@ -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
|
+
});
|