@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/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@startsimpli/api",
3
- "version": "0.5.19",
3
+ "version": "0.5.21",
4
4
  "description": "Type-safe Django REST API client for StartSimpli apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "exports": {
8
- ".": "./src/index.ts"
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "react-native": "./src/index.ts",
11
+ "default": "./src/index.ts"
12
+ },
13
+ "./middleware": {
14
+ "types": "./src/middleware/index.ts",
15
+ "default": "./src/middleware/index.ts"
16
+ },
17
+ "./package.json": "./package.json"
9
18
  },
10
19
  "files": [
11
20
  "src",
@@ -14,14 +23,6 @@
14
23
  "publishConfig": {
15
24
  "access": "public"
16
25
  },
17
- "scripts": {
18
- "build": "tsup",
19
- "dev": "tsup --watch",
20
- "type-check": "tsc --noEmit",
21
- "test": "vitest run",
22
- "test:watch": "vitest",
23
- "clean": "rm -rf dist"
24
- },
25
26
  "dependencies": {
26
27
  "isomorphic-dompurify": "^3.10.0",
27
28
  "zod": "^4.3.6"
@@ -51,5 +52,13 @@
51
52
  "tsup": "^8.5.1",
52
53
  "typescript": "^6.0.3",
53
54
  "vitest": "^4.1.5"
55
+ },
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "dev": "tsup --watch",
59
+ "type-check": "tsc --noEmit",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest",
62
+ "clean": "rm -rf dist"
54
63
  }
55
- }
64
+ }
@@ -59,6 +59,9 @@ export const ENDPOINTS = {
59
59
  CONTACT_ENRICH: (id: string) => `api/v1/contacts/${id}/enrich`,
60
60
  CONTACTS_ENRICH_APOLLO: 'api/v1/contacts/enrich-apollo',
61
61
 
62
+ // Auth
63
+ AUTH_EARLY_REGISTER: 'api/v1/auth/early-register',
64
+
62
65
  // Users
63
66
  USER_ME: 'api/v1/users/me',
64
67
  USER_CHANGE_PASSWORD: 'api/v1/users/me/change-password',
@@ -66,6 +69,36 @@ export const ENDPOINTS = {
66
69
  // Companies / Feature flags
67
70
  FEATURE_FLAGS: 'api/v1/companies/feature-flags',
68
71
 
72
+ // Companies (startsim-o7s) — accepts numeric id OR slug
73
+ COMPANIES: 'api/v1/companies',
74
+ COMPANY: (idOrSlug: string) => `api/v1/companies/${idOrSlug}`,
75
+ COMPANY_FEATURE_FLAGS: (idOrSlug: string) => `api/v1/companies/${idOrSlug}/feature-flags`,
76
+
77
+ // Teams (startsim-o7s, startsim-tsm)
78
+ TEAMS: 'api/v1/teams',
79
+ TEAM: (idOrSlug: string) => `api/v1/teams/${idOrSlug}`,
80
+ TEAM_MEMBERS: (idOrSlug: string) => `api/v1/teams/${idOrSlug}/members`,
81
+ TEAM_BULK_INVITE: (idOrSlug: string) => `api/v1/teams/${idOrSlug}/bulk-invite`,
82
+ TEAM_REMOVE_MEMBER: (idOrSlug: string) => `api/v1/teams/${idOrSlug}/remove-member`,
83
+ TEAM_UPDATE_ROLE: (idOrSlug: string) => `api/v1/teams/${idOrSlug}/update-role`,
84
+ TEAM_MEMBERS_MY_TEAMS: 'api/v1/team-members/my-teams',
85
+
86
+ // Team invitations (startsim-tsm)
87
+ TEAM_INVITATIONS: 'api/v1/team-invitations',
88
+ TEAM_INVITATION: (id: string) => `api/v1/team-invitations/${id}`,
89
+ TEAM_INVITATION_ACCEPT: (id: string) => `api/v1/team-invitations/${id}/accept`,
90
+ TEAM_INVITATION_REVOKE: (id: string) => `api/v1/team-invitations/${id}/revoke`,
91
+
92
+ // Email domain claims (startsim-gpu)
93
+ TEAM_DOMAIN_CLAIMS: 'api/v1/team-domain-claims',
94
+ TEAM_DOMAIN_CLAIM: (id: string) => `api/v1/team-domain-claims/${id}`,
95
+ TEAM_DOMAIN_CLAIM_VERIFY_DNS: (id: string) => `api/v1/team-domain-claims/${id}/verify-dns`,
96
+ TEAM_DOMAIN_CLAIM_VERIFY_EMAIL_INITIATE: (id: string) =>
97
+ `api/v1/team-domain-claims/${id}/verify-email-initiate`,
98
+ TEAM_DOMAIN_CLAIM_VERIFY_EMAIL_CODE: (id: string) =>
99
+ `api/v1/team-domain-claims/${id}/verify-email-code`,
100
+ TEAM_DOMAIN_CLAIM_REVOKE: (id: string) => `api/v1/team-domain-claims/${id}/revoke`,
101
+
69
102
  // Markets (instruments, prices, analytics, news, health)
70
103
  INSTRUMENTS: 'api/v1/markets/instruments',
71
104
  INSTRUMENT: (symbol: string) => `api/v1/markets/instruments/${symbol}`,
@@ -77,11 +110,18 @@ export const ENDPOINTS = {
77
110
  SECTOR_BREADTH: 'api/v1/markets/sector_breadth',
78
111
  EARNINGS_CALENDAR: 'api/v1/markets/calendar',
79
112
  TRADING_SNAPSHOTS: 'api/v1/markets/trading/snapshots',
113
+ BARS_BULK: 'api/v1/markets/bars/bulk',
114
+ SOCIAL_POSTS: 'api/v1/markets/social_posts',
80
115
 
81
- // Options — schemas aligned with brain-trading prefs (agent_bridge req b09a6bb6)
82
- OPTIONS_CHAIN: 'api/v1/markets/options/chain',
116
+ // Options — schemas confirmed by claude-mac (agent_bridge req 22ac4889).
117
+ // /options/iv/ summary + /options/skew/ intentionally omitted: iv/history covers
118
+ // rank/RV/spread; skew is client-derived from chain (mac req a2f98ba6).
119
+ OPTIONS_CHAIN: (symbol: string) => `api/v1/markets/instruments/${symbol}/options/chain`,
83
120
  OPTIONS_IV_HISTORY: (symbol: string) => `api/v1/markets/instruments/${symbol}/options/iv/history`,
121
+ OPTIONS_ATM_IV_HISTORY: (symbol: string) => `api/v1/markets/instruments/${symbol}/options/atm_iv_history`,
122
+ MACRO_CALENDAR: 'api/v1/markets/macro_calendar',
84
123
  OPTIONS_GREEKS: 'api/v1/markets/options/greeks',
124
+ OPTIONS_UNUSUAL_ACTIVITY: 'api/v1/markets/options/unusual_activity',
85
125
  VIX_TERM: 'api/v1/markets/vix_term',
86
126
 
87
127
  // Sources ops
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@ export type { MessageStats } from './lib/message-stats';
25
25
  export { MessageTemplatesApi } from './lib/message-templates-api';
26
26
  export type { MessageTemplate, MessageTemplateFilters, CreateMessageTemplateInput } from './lib/message-templates-api';
27
27
  export { UsersApi } from './lib/users-api';
28
- export type { UserProfile, UpdateProfileRequest, ChangePasswordRequest, ChangePasswordResponse } from './types/user';
28
+ export type { UserProfile, UpdateProfileRequest, ChangePasswordRequest, ChangePasswordResponse, EarlyRegisterRequest, EarlyRegisterResponse } from './types/user';
29
29
  export { FunnelsApi, isFunnelRunConflict, isFunnelValidationError } from './lib/funnels-api';
30
30
  export type {
31
31
  FunnelPreviewResult,
@@ -163,6 +163,11 @@ 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';
167
+ import { CompaniesApi } from './lib/companies-api';
168
+ import { TeamsApi } from './lib/teams-api';
169
+ import { TeamInvitationsApi } from './lib/team-invitations-api';
170
+ import { DomainClaimsApi } from './lib/domain-claims-api';
166
171
 
167
172
  import type { ApiClientConfig } from './lib/api-client';
168
173
 
@@ -187,9 +192,36 @@ export function createStartSimpliApi(config: ApiClientConfig = {}) {
187
192
  targetLists: new TargetListsApi(client),
188
193
  messageTemplates: new MessageTemplatesApi(client),
189
194
  markets: new MarketsApi(client),
195
+ vault: new VaultApi(client),
196
+ companies: new CompaniesApi(client),
197
+ teams: new TeamsApi(client),
198
+ teamInvitations: new TeamInvitationsApi(client),
199
+ domainClaims: new DomainClaimsApi(client),
190
200
  };
191
201
  }
192
202
 
203
+ // Team management (startsim-o7s)
204
+ export { CompaniesApi } from './lib/companies-api';
205
+ export { TeamsApi } from './lib/teams-api';
206
+ export type { MyTeamMembership } from './lib/teams-api';
207
+ export { TeamInvitationsApi } from './lib/team-invitations-api';
208
+ export { DomainClaimsApi } from './lib/domain-claims-api';
209
+
210
+ // Vault API
211
+ export { VaultApi } from './lib/vault-api';
212
+ export type {
213
+ VaultEnvironment,
214
+ VaultEnvironmentInput,
215
+ VaultSecret,
216
+ VaultSecretInput,
217
+ VaultSecretReveal,
218
+ VaultAccessKey,
219
+ VaultAccessKeyCreated,
220
+ VaultAccessKeyInput,
221
+ VaultAuditEntry,
222
+ VaultListParams,
223
+ } from './lib/vault-api';
224
+
193
225
  // Markets API
194
226
  export { MarketsApi } from './lib/markets-api';
195
227
  export type {
@@ -239,9 +271,29 @@ export type {
239
271
  OptionsIvResponse,
240
272
  OptionsIvHistoryParams,
241
273
  OptionsSkewPoint,
274
+ OptionsChainRaw,
242
275
  OptionsGreeks,
276
+ OptionsUnusualActivityPoint,
277
+ OptionsUnusualActivityParams,
278
+ OptionsUnusualActivityResponse,
279
+ AtmIvHistoryPoint,
280
+ AtmIvHistoryParams,
281
+ AtmIvHistoryResponse,
282
+ MacroCategory,
283
+ MacroCalendarRow,
284
+ MacroCalendarParams,
285
+ MacroCalendarResponse,
243
286
  VixTenor,
244
287
  VixTermState,
245
288
  VixTermPoint,
246
289
  VixTermResponse,
290
+ BarsBulkParams,
291
+ BarsBulkResponse,
292
+ SocialPlatform,
293
+ SocialDirection,
294
+ SocialPost,
295
+ SocialPostMatchedEntity,
296
+ SocialPostResolvedContact,
297
+ SocialPostsParams,
298
+ SocialPostsResponse,
247
299
  } from './lib/markets-api';
@@ -0,0 +1,53 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { CompaniesApi } from '../companies-api';
3
+
4
+ function makeApi() {
5
+ const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
6
+ const api = new CompaniesApi({ fetch } as never);
7
+ return { api, fetch };
8
+ }
9
+
10
+ describe('CompaniesApi', () => {
11
+ let api: CompaniesApi;
12
+ let fetch: { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; patch: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
13
+
14
+ beforeEach(() => {
15
+ ({ api, fetch } = makeApi());
16
+ });
17
+
18
+ it('list passes params through to the companies endpoint', async () => {
19
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
20
+ await api.list({ search: 'acme', pageSize: 50 });
21
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/companies', {
22
+ params: { search: 'acme', pageSize: 50 },
23
+ });
24
+ });
25
+
26
+ it('retrieve accepts either id or slug in the path', async () => {
27
+ fetch.get.mockResolvedValue({ id: '1', slug: 'acme' });
28
+ await api.retrieve('acme');
29
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/companies/acme');
30
+ });
31
+
32
+ it('update PATCHes the company with the camelCase payload', async () => {
33
+ fetch.patch.mockResolvedValue({ id: '1', slug: 'acme', name: 'Acme Inc.' });
34
+ await api.update('acme', { name: 'Acme Inc.' });
35
+ expect(fetch.patch).toHaveBeenCalledWith('api/v1/companies/acme', { name: 'Acme Inc.' });
36
+ });
37
+
38
+ it('featureFlags.get unwraps the flags envelope', async () => {
39
+ fetch.get.mockResolvedValue({ flags: { section_inbox: true } });
40
+ const flags = await api.featureFlags.get('acme');
41
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/companies/acme/feature-flags');
42
+ expect(flags).toEqual({ section_inbox: true });
43
+ });
44
+
45
+ it('featureFlags.update PATCHes a wrapped flags payload', async () => {
46
+ fetch.patch.mockResolvedValue({ flags: { section_inbox: false } });
47
+ const flags = await api.featureFlags.update('acme', { section_inbox: false });
48
+ expect(fetch.patch).toHaveBeenCalledWith('api/v1/companies/acme/feature-flags', {
49
+ flags: { section_inbox: false },
50
+ });
51
+ expect(flags).toEqual({ section_inbox: false });
52
+ });
53
+ });
@@ -0,0 +1,68 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { DomainClaimsApi } from '../domain-claims-api';
3
+
4
+ function makeApi() {
5
+ const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
6
+ const api = new DomainClaimsApi({ fetch } as never);
7
+ return { api, fetch };
8
+ }
9
+
10
+ describe('DomainClaimsApi', () => {
11
+ let api: DomainClaimsApi;
12
+ let fetch: { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; patch: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
13
+
14
+ beforeEach(() => {
15
+ ({ api, fetch } = makeApi());
16
+ });
17
+
18
+ it('list hits the team-domain-claims endpoint', async () => {
19
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
20
+ await api.list({ companyId: 'c1', verified: true });
21
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/team-domain-claims', {
22
+ params: { companyId: 'c1', verified: true },
23
+ });
24
+ });
25
+
26
+ it('create POSTs the claim payload + returns the visible token', async () => {
27
+ fetch.post.mockResolvedValue({
28
+ id: 'd1',
29
+ domain: 'acme.com',
30
+ verificationToken: 'startsim-verify=xyz',
31
+ });
32
+ const r = await api.create({ companyId: 'c1', domain: 'acme.com' });
33
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/team-domain-claims', {
34
+ companyId: 'c1',
35
+ domain: 'acme.com',
36
+ });
37
+ expect(r.verificationToken).toBe('startsim-verify=xyz');
38
+ });
39
+
40
+ it('verifyDns posts the verify-dns action', async () => {
41
+ fetch.post.mockResolvedValue({ id: 'd1', verified: true });
42
+ await api.verifyDns('d1');
43
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/team-domain-claims/d1/verify-dns');
44
+ });
45
+
46
+ it('verifyEmailInitiate posts the initiate action', async () => {
47
+ fetch.post.mockResolvedValue({ detail: 'sent' });
48
+ await api.verifyEmailInitiate('d1');
49
+ expect(fetch.post).toHaveBeenCalledWith(
50
+ 'api/v1/team-domain-claims/d1/verify-email-initiate',
51
+ );
52
+ });
53
+
54
+ it('verifyEmailCode posts the code', async () => {
55
+ fetch.post.mockResolvedValue({ id: 'd1', verified: true });
56
+ await api.verifyEmailCode('d1', '123456');
57
+ expect(fetch.post).toHaveBeenCalledWith(
58
+ 'api/v1/team-domain-claims/d1/verify-email-code',
59
+ { code: '123456' },
60
+ );
61
+ });
62
+
63
+ it('revoke posts the revoke action', async () => {
64
+ fetch.post.mockResolvedValue(undefined);
65
+ await api.revoke('d1');
66
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/team-domain-claims/d1/revoke');
67
+ });
68
+ });
@@ -0,0 +1,50 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { TeamInvitationsApi } from '../team-invitations-api';
3
+
4
+ function makeApi() {
5
+ const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
6
+ const api = new TeamInvitationsApi({ fetch } as never);
7
+ return { api, fetch };
8
+ }
9
+
10
+ describe('TeamInvitationsApi', () => {
11
+ let api: TeamInvitationsApi;
12
+ let fetch: { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; patch: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
13
+
14
+ beforeEach(() => {
15
+ ({ api, fetch } = makeApi());
16
+ });
17
+
18
+ it('list reaches the invitations endpoint', async () => {
19
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
20
+ await api.list({ teamId: 't1' });
21
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/team-invitations', {
22
+ params: { teamId: 't1' },
23
+ });
24
+ });
25
+
26
+ it('create POSTs the invitation payload', async () => {
27
+ fetch.post.mockResolvedValue({ id: 'i1', token: 'raw-token' });
28
+ const r = await api.create({ email: 'a@x.com', teamId: 't1', role: 'member' });
29
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/team-invitations', {
30
+ email: 'a@x.com',
31
+ teamId: 't1',
32
+ role: 'member',
33
+ });
34
+ expect(r.token).toBe('raw-token');
35
+ });
36
+
37
+ it('revoke POSTs the revoke action', async () => {
38
+ fetch.post.mockResolvedValue(undefined);
39
+ await api.revoke('i1');
40
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/team-invitations/i1/revoke');
41
+ });
42
+
43
+ it('accept POSTs the token in the body', async () => {
44
+ fetch.post.mockResolvedValue({ id: 'i1', acceptedAt: '2025-01-01' });
45
+ await api.accept('i1', 'raw-token');
46
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/team-invitations/i1/accept', {
47
+ token: 'raw-token',
48
+ });
49
+ });
50
+ });
@@ -0,0 +1,74 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { TeamsApi } from '../teams-api';
3
+
4
+ function makeApi() {
5
+ const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
6
+ const api = new TeamsApi({ fetch } as never);
7
+ return { api, fetch };
8
+ }
9
+
10
+ describe('TeamsApi', () => {
11
+ let api: TeamsApi;
12
+ let fetch: { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; patch: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn> };
13
+
14
+ beforeEach(() => {
15
+ ({ api, fetch } = makeApi());
16
+ });
17
+
18
+ it('list hits the teams endpoint with params', async () => {
19
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
20
+ await api.list({ companyId: 'c1' });
21
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/teams', { params: { companyId: 'c1' } });
22
+ });
23
+
24
+ it('retrieve accepts slug', async () => {
25
+ fetch.get.mockResolvedValue({ id: '1', slug: 'eng' });
26
+ await api.retrieve('eng');
27
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/teams/eng');
28
+ });
29
+
30
+ it('members returns the bare array (no pagination wrapper)', async () => {
31
+ fetch.get.mockResolvedValue([{ id: 'm1', userId: 'u1', role: 'owner' }]);
32
+ const members = await api.members('eng');
33
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/teams/eng/members');
34
+ expect(members).toHaveLength(1);
35
+ });
36
+
37
+ it('bulkInvite POSTs the invitations array wrapped in an envelope', async () => {
38
+ fetch.post.mockResolvedValue({ invited: [], skipped: [] });
39
+ await api.bulkInvite('eng', [
40
+ { email: 'a@x.com', role: 'member' },
41
+ { email: 'b@x.com', role: 'admin' },
42
+ ]);
43
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/teams/eng/bulk-invite', {
44
+ invitations: [
45
+ { email: 'a@x.com', role: 'member' },
46
+ { email: 'b@x.com', role: 'admin' },
47
+ ],
48
+ });
49
+ });
50
+
51
+ it('removeMember sends userId in the body, not the URL', async () => {
52
+ fetch.post.mockResolvedValue(undefined);
53
+ await api.removeMember('eng', 'u42');
54
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/teams/eng/remove-member', {
55
+ userId: 'u42',
56
+ });
57
+ });
58
+
59
+ it('updateRole sends userId + role in the body', async () => {
60
+ fetch.post.mockResolvedValue({ id: 'm1', userId: 'u42', role: 'admin' });
61
+ await api.updateRole('eng', 'u42', 'admin');
62
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/teams/eng/update-role', {
63
+ userId: 'u42',
64
+ role: 'admin',
65
+ });
66
+ });
67
+
68
+ it('myTeams returns the unpaginated membership array', async () => {
69
+ fetch.get.mockResolvedValue([{ id: 'm1', teamId: 't1', role: 'owner' }]);
70
+ const rows = await api.myTeams();
71
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/team-members/my-teams');
72
+ expect(rows[0].role).toBe('owner');
73
+ });
74
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Companies API wrapper for /api/v1/companies/*.
3
+ *
4
+ * The Django CompanyViewSet accepts numeric id OR slug as the URL identifier.
5
+ * Feature-flags get their own nested endpoint so app shells can fetch flags
6
+ * without pulling the full company object. startsim-o7s.
7
+ */
8
+
9
+ import type { ApiClient } from './api-client';
10
+ import { ENDPOINTS } from '../constants/endpoints';
11
+ import type {
12
+ Company,
13
+ CompanyListParams,
14
+ UpdateCompanyInput,
15
+ } from '../types/team';
16
+ import type { FeatureFlags, FeatureFlagsResponse } from './feature-flags';
17
+ import type { PaginatedResponse } from '../types';
18
+
19
+ export class CompaniesApi {
20
+ constructor(private client: ApiClient) {}
21
+
22
+ /** List companies the current user belongs to. */
23
+ list(params?: CompanyListParams): Promise<PaginatedResponse<Company>> {
24
+ return this.client.fetch.get<PaginatedResponse<Company>>(ENDPOINTS.COMPANIES, { params });
25
+ }
26
+
27
+ /** Retrieve a single company by id or slug. */
28
+ retrieve(idOrSlug: string): Promise<Company> {
29
+ return this.client.fetch.get<Company>(ENDPOINTS.COMPANY(idOrSlug));
30
+ }
31
+
32
+ /** PATCH update — name, slug, settings. */
33
+ update(idOrSlug: string, patch: UpdateCompanyInput): Promise<Company> {
34
+ return this.client.fetch.patch<Company>(ENDPOINTS.COMPANY(idOrSlug), patch);
35
+ }
36
+
37
+ /** Per-company feature-flag accessors (admin-gated on PATCH). */
38
+ readonly featureFlags = {
39
+ get: (idOrSlug: string): Promise<FeatureFlags> =>
40
+ this.client.fetch
41
+ .get<FeatureFlagsResponse>(ENDPOINTS.COMPANY_FEATURE_FLAGS(idOrSlug))
42
+ .then((r) => r.flags),
43
+ update: (idOrSlug: string, patch: Partial<FeatureFlags>): Promise<FeatureFlags> =>
44
+ this.client.fetch
45
+ .patch<FeatureFlagsResponse>(ENDPOINTS.COMPANY_FEATURE_FLAGS(idOrSlug), { flags: patch })
46
+ .then((r) => r.flags),
47
+ };
48
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * EmailDomainClaim API wrapper for /api/v1/team-domain-claims/*.
3
+ *
4
+ * Two verification methods are supported by the backend (startsim-gpu):
5
+ * - DNS TXT: caller adds a TXT record, then calls verify-dns.
6
+ * - Email attestation: backend sends a 6-digit code to a postmaster@<domain>
7
+ * mailbox; caller submits the code via verify-email-code.
8
+ */
9
+
10
+ import type { ApiClient } from './api-client';
11
+ import { ENDPOINTS } from '../constants/endpoints';
12
+ import type {
13
+ EmailDomainClaim,
14
+ DomainClaimListParams,
15
+ CreateDomainClaimInput,
16
+ DomainVerifyEmailInitiateResponse,
17
+ } from '../types/team';
18
+ import type { PaginatedResponse } from '../types';
19
+
20
+ export class DomainClaimsApi {
21
+ constructor(private client: ApiClient) {}
22
+
23
+ list(params?: DomainClaimListParams): Promise<PaginatedResponse<EmailDomainClaim>> {
24
+ return this.client.fetch.get<PaginatedResponse<EmailDomainClaim>>(
25
+ ENDPOINTS.TEAM_DOMAIN_CLAIMS,
26
+ { params },
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Create a new claim. The response includes the verification_token (visible
32
+ * only to the creator); subsequent reads return null.
33
+ */
34
+ create(input: CreateDomainClaimInput): Promise<EmailDomainClaim> {
35
+ return this.client.fetch.post<EmailDomainClaim>(ENDPOINTS.TEAM_DOMAIN_CLAIMS, input);
36
+ }
37
+
38
+ /** Trigger DNS TXT lookup. Backend marks verified=true on a clean match. */
39
+ verifyDns(id: string): Promise<EmailDomainClaim> {
40
+ return this.client.fetch.post<EmailDomainClaim>(
41
+ ENDPOINTS.TEAM_DOMAIN_CLAIM_VERIFY_DNS(id),
42
+ );
43
+ }
44
+
45
+ /** Kick off email-attestation flow — backend emails postmaster@<domain>. */
46
+ verifyEmailInitiate(id: string): Promise<DomainVerifyEmailInitiateResponse> {
47
+ return this.client.fetch.post<DomainVerifyEmailInitiateResponse>(
48
+ ENDPOINTS.TEAM_DOMAIN_CLAIM_VERIFY_EMAIL_INITIATE(id),
49
+ );
50
+ }
51
+
52
+ /** Submit the postmaster mailbox's 6-digit code to complete attestation. */
53
+ verifyEmailCode(id: string, code: string): Promise<EmailDomainClaim> {
54
+ return this.client.fetch.post<EmailDomainClaim>(
55
+ ENDPOINTS.TEAM_DOMAIN_CLAIM_VERIFY_EMAIL_CODE(id),
56
+ { code },
57
+ );
58
+ }
59
+
60
+ revoke(id: string): Promise<void> {
61
+ return this.client.fetch.post<void>(ENDPOINTS.TEAM_DOMAIN_CLAIM_REVOKE(id));
62
+ }
63
+ }
@@ -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
  /**