@startsimpli/api 0.5.20 → 0.5.22

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/api",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "description": "Type-safe Django REST API client for StartSimpli apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -23,14 +23,6 @@
23
23
  "publishConfig": {
24
24
  "access": "public"
25
25
  },
26
- "scripts": {
27
- "build": "tsup",
28
- "dev": "tsup --watch",
29
- "type-check": "tsc --noEmit",
30
- "test": "vitest run",
31
- "test:watch": "vitest",
32
- "clean": "rm -rf dist"
33
- },
34
26
  "dependencies": {
35
27
  "isomorphic-dompurify": "^3.10.0",
36
28
  "zod": "^4.3.6"
@@ -60,5 +52,13 @@
60
52
  "tsup": "^8.5.1",
61
53
  "typescript": "^6.0.3",
62
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"
63
63
  }
64
- }
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}`,
package/src/index.ts CHANGED
@@ -18,6 +18,21 @@ export { ContactsApi } from './lib/contacts-api';
18
18
  export { OrganizationsApi } from './lib/organizations-api';
19
19
  export { EntitiesApi } from './lib/entities-api';
20
20
  export { WorkflowsApi } from './lib/workflows-api';
21
+ export type {
22
+ Workflow,
23
+ WorkflowNode,
24
+ WorkflowInput,
25
+ WorkflowListParams,
26
+ NodeTypeDef,
27
+ NodeStatus,
28
+ RunStatus,
29
+ ExecutionMode,
30
+ WorkflowExecution,
31
+ WorkflowNodeExecution,
32
+ WorkflowExecutionDetail,
33
+ ExecuteWorkflowResponse,
34
+ ExecutionListParams,
35
+ } from './lib/workflows-api';
21
36
  export { MessagesApi } from './lib/messages-api';
22
37
  export type { Message, MessageStatus as MessageApiStatus, MessageRecipient, MessagingChannel as MessagingChannelType, MessageFilters, CreateMessageInput, ScheduleMessageInput, SendTestInput } from './lib/messages-api';
23
38
  export { calculateStatsFromMessages } from './lib/message-stats';
@@ -25,7 +40,7 @@ export type { MessageStats } from './lib/message-stats';
25
40
  export { MessageTemplatesApi } from './lib/message-templates-api';
26
41
  export type { MessageTemplate, MessageTemplateFilters, CreateMessageTemplateInput } from './lib/message-templates-api';
27
42
  export { UsersApi } from './lib/users-api';
28
- export type { UserProfile, UpdateProfileRequest, ChangePasswordRequest, ChangePasswordResponse } from './types/user';
43
+ export type { UserProfile, UpdateProfileRequest, ChangePasswordRequest, ChangePasswordResponse, EarlyRegisterRequest, EarlyRegisterResponse } from './types/user';
29
44
  export { FunnelsApi, isFunnelRunConflict, isFunnelValidationError } from './lib/funnels-api';
30
45
  export type {
31
46
  FunnelPreviewResult,
@@ -164,6 +179,11 @@ import { TargetListsApi } from './lib/target-lists-api';
164
179
  import { MessageTemplatesApi } from './lib/message-templates-api';
165
180
  import { MarketsApi } from './lib/markets-api';
166
181
  import { VaultApi } from './lib/vault-api';
182
+ import { PresentationsApi } from './lib/presentations-api';
183
+ import { CompaniesApi } from './lib/companies-api';
184
+ import { TeamsApi } from './lib/teams-api';
185
+ import { TeamInvitationsApi } from './lib/team-invitations-api';
186
+ import { DomainClaimsApi } from './lib/domain-claims-api';
167
187
 
168
188
  import type { ApiClientConfig } from './lib/api-client';
169
189
 
@@ -189,9 +209,21 @@ export function createStartSimpliApi(config: ApiClientConfig = {}) {
189
209
  messageTemplates: new MessageTemplatesApi(client),
190
210
  markets: new MarketsApi(client),
191
211
  vault: new VaultApi(client),
212
+ presentations: new PresentationsApi(client),
213
+ companies: new CompaniesApi(client),
214
+ teams: new TeamsApi(client),
215
+ teamInvitations: new TeamInvitationsApi(client),
216
+ domainClaims: new DomainClaimsApi(client),
192
217
  };
193
218
  }
194
219
 
220
+ // Team management (startsim-o7s)
221
+ export { CompaniesApi } from './lib/companies-api';
222
+ export { TeamsApi } from './lib/teams-api';
223
+ export type { MyTeamMembership } from './lib/teams-api';
224
+ export { TeamInvitationsApi } from './lib/team-invitations-api';
225
+ export { DomainClaimsApi } from './lib/domain-claims-api';
226
+
195
227
  // Vault API
196
228
  export { VaultApi } from './lib/vault-api';
197
229
  export type {
@@ -207,6 +239,23 @@ export type {
207
239
  VaultListParams,
208
240
  } from './lib/vault-api';
209
241
 
242
+ // Presentations API
243
+ export { PresentationsApi } from './lib/presentations-api';
244
+ export type {
245
+ Deck,
246
+ DeckInput,
247
+ DeckStatus,
248
+ DeckListParams,
249
+ DeckGenerationStatus,
250
+ GenerateDeckInput,
251
+ Slide,
252
+ SlideInput,
253
+ SlideStatus,
254
+ RegenerateSlideInput,
255
+ RenderMode,
256
+ CompileResult,
257
+ } from './lib/presentations-api';
258
+
210
259
  // Markets API
211
260
  export { MarketsApi } from './lib/markets-api';
212
261
  export type {
@@ -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
+ }
@@ -0,0 +1,104 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { PresentationsApi } from './presentations-api';
3
+
4
+ function makeApi() {
5
+ const fetch = { get: vi.fn(), post: vi.fn(), patch: vi.fn(), delete: vi.fn() };
6
+ const api = new PresentationsApi({ fetch } as never);
7
+ return { api, fetch };
8
+ }
9
+
10
+ describe('PresentationsApi', () => {
11
+ let api: PresentationsApi;
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('listDecks hits the decks endpoint with params', async () => {
19
+ fetch.get.mockResolvedValue({ results: [], count: 0 });
20
+ await api.listDecks({ page: 2 });
21
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks', { params: { page: 2 } });
22
+ });
23
+
24
+ it('getDeck fetches a single deck by id', async () => {
25
+ fetch.get.mockResolvedValue({ id: 'd1', title: 'Pitch' });
26
+ const d = await api.getDeck('d1');
27
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks/d1');
28
+ expect(d.title).toBe('Pitch');
29
+ });
30
+
31
+ it('createDeck posts the deck input', async () => {
32
+ fetch.post.mockResolvedValue({ id: 'd1', title: 'Pitch' });
33
+ await api.createDeck({ title: 'Pitch', outline: 'Slide 1...', audience: 'VCs' });
34
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks', {
35
+ title: 'Pitch',
36
+ outline: 'Slide 1...',
37
+ audience: 'VCs',
38
+ });
39
+ });
40
+
41
+ it('updateDeck patches by id', async () => {
42
+ fetch.patch.mockResolvedValue({ id: 'd1', title: 'New' });
43
+ await api.updateDeck('d1', { title: 'New' });
44
+ expect(fetch.patch).toHaveBeenCalledWith('api/v1/presentations/decks/d1', { title: 'New' });
45
+ });
46
+
47
+ it('deleteDeck deletes by id', async () => {
48
+ fetch.delete.mockResolvedValue(undefined);
49
+ await api.deleteDeck('d1');
50
+ expect(fetch.delete).toHaveBeenCalledWith('api/v1/presentations/decks/d1');
51
+ });
52
+
53
+ it('generateDeck kicks off generation (derive style + slides)', async () => {
54
+ fetch.post.mockResolvedValue({ id: 'd1', status: 'generating' });
55
+ await api.generateDeck('d1', { outline: 'Slide 1: Title' });
56
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/generate', {
57
+ outline: 'Slide 1: Title',
58
+ });
59
+ });
60
+
61
+ it('getDeckStatus polls the status endpoint', async () => {
62
+ fetch.get.mockResolvedValue({ status: 'generating', slides: [] });
63
+ await api.getDeckStatus('d1');
64
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks/d1/status');
65
+ });
66
+
67
+ it('listSlides fetches the deck slides', async () => {
68
+ fetch.get.mockResolvedValue([{ id: 's1', slideNumber: 1 }]);
69
+ await api.listSlides('d1');
70
+ expect(fetch.get).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides');
71
+ });
72
+
73
+ it('updateSlide patches a slide by number', async () => {
74
+ fetch.patch.mockResolvedValue({ id: 's2', slideNumber: 2 });
75
+ await api.updateSlide('d1', 2, { content: 'Revised' });
76
+ expect(fetch.patch).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides/2', {
77
+ content: 'Revised',
78
+ });
79
+ });
80
+
81
+ it('regenerateSlide posts an instruction for one slide', async () => {
82
+ fetch.post.mockResolvedValue({ id: 's2', slideNumber: 2, status: 'generating' });
83
+ await api.regenerateSlide('d1', 2, { instruction: 'make it punchier' });
84
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides/2/regenerate', {
85
+ instruction: 'make it punchier',
86
+ });
87
+ });
88
+
89
+ it('reorderSlides posts the new order', async () => {
90
+ fetch.post.mockResolvedValue(undefined);
91
+ await api.reorderSlides('d1', [3, 1, 2]);
92
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/slides/reorder', {
93
+ order: [3, 1, 2],
94
+ });
95
+ });
96
+
97
+ it('compileDeck builds PPTX + PDF and returns artifact urls', async () => {
98
+ fetch.post.mockResolvedValue({ status: 'ready', pptxUrl: 'p.pptx', pdfUrl: 'p.pdf' });
99
+ const r = await api.compileDeck('d1');
100
+ expect(fetch.post).toHaveBeenCalledWith('api/v1/presentations/decks/d1/compile', undefined);
101
+ expect(r.pptxUrl).toBe('p.pptx');
102
+ expect(r.pdfUrl).toBe('p.pdf');
103
+ });
104
+ });