@myapihq/sdk 1.1.0-wip.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/audience.d.ts +71 -0
  2. package/dist/audience.js +54 -0
  3. package/dist/client.d.ts +1 -1
  4. package/dist/client.js +8 -1
  5. package/dist/company.d.ts +20 -0
  6. package/dist/company.js +25 -0
  7. package/dist/config.d.ts +6 -0
  8. package/dist/config.js +20 -2
  9. package/dist/crm.d.ts +122 -0
  10. package/dist/crm.js +84 -0
  11. package/dist/database.d.ts +36 -0
  12. package/dist/database.js +65 -0
  13. package/dist/domain.d.ts +19 -1
  14. package/dist/domain.js +22 -2
  15. package/dist/email-verify.d.ts +30 -0
  16. package/dist/email-verify.js +17 -0
  17. package/dist/email.d.ts +33 -0
  18. package/dist/email.js +53 -0
  19. package/dist/exposes.d.ts +2 -0
  20. package/dist/exposes.js +14 -0
  21. package/dist/funnel.d.ts +10 -0
  22. package/dist/funnel.js +20 -0
  23. package/dist/hq.d.ts +10 -0
  24. package/dist/hq.js +28 -0
  25. package/dist/image.d.ts +14 -0
  26. package/dist/image.js +12 -0
  27. package/dist/index.d.ts +6 -1
  28. package/dist/index.js +13 -2
  29. package/dist/llm.d.ts +50 -0
  30. package/dist/llm.js +27 -0
  31. package/dist/people.d.ts +54 -0
  32. package/dist/people.js +22 -0
  33. package/dist/pixel.d.ts +3 -0
  34. package/dist/pixel.js +15 -0
  35. package/dist/services.d.ts +17 -0
  36. package/dist/services.js +190 -0
  37. package/dist/storage.d.ts +3 -1
  38. package/dist/storage.js +7 -0
  39. package/dist/url.d.ts +2 -0
  40. package/dist/url.js +4 -0
  41. package/dist/webhook.d.ts +8 -1
  42. package/dist/webhook.js +14 -2
  43. package/dist/workflow.d.ts +2 -0
  44. package/dist/workflow.js +12 -0
  45. package/package.json +2 -1
  46. package/scripts/scaffold-sdk.js +501 -0
  47. package/src/audience.ts +162 -0
  48. package/src/client.ts +9 -1
  49. package/src/company.ts +48 -0
  50. package/src/config.ts +21 -1
  51. package/src/crm.ts +235 -0
  52. package/src/database.ts +103 -0
  53. package/src/domain.ts +44 -2
  54. package/src/email.ts +84 -0
  55. package/src/exposes.ts +16 -0
  56. package/src/funnel.ts +34 -0
  57. package/src/hq.ts +41 -0
  58. package/src/image.ts +31 -0
  59. package/src/index.ts +12 -1
  60. package/src/llm.ts +86 -0
  61. package/src/people.ts +84 -0
  62. package/src/pixel.ts +16 -0
  63. package/src/services.ts +220 -0
  64. package/src/storage.ts +15 -6
  65. package/src/url.ts +5 -0
  66. package/src/webhook.ts +25 -2
  67. package/src/workflow.ts +13 -0
package/src/company.ts ADDED
@@ -0,0 +1,48 @@
1
+ // Scaffolded from schema by scripts/scaffold-sdk.js — DO NOT remove the
2
+ // EXPOSES array (the coverage tool verifies it against the live schema).
3
+ // Types and function names ARE editable; re-running the scaffolder against
4
+ // a service whose config has been changed will diff vs. existing file
5
+ // (use --dry-run to inspect).
6
+
7
+ import { request } from './client';
8
+ import { COMPANY_BASE as BASE_URL } from './config';
9
+ import type { Exposes } from './exposes';
10
+
11
+ import type { SearchFilter } from './audience';
12
+ import type { Person } from './people';
13
+
14
+ export const EXPOSES: Exposes = [
15
+ 'POST /company/orgs/{org_id}/search',
16
+ 'GET /company/orgs/{org_id}/{company_id}',
17
+ ];
18
+
19
+ import type { Location, PersonCompany } from './people';
20
+
21
+ // Company is the same shape as PersonCompany (a person's embedded company)
22
+ // with an optional embedded people array when include_people=N is passed.
23
+ export interface Company extends PersonCompany {
24
+ people?: Person[]; // present when include_people=N is passed
25
+ }
26
+
27
+ export interface CompanySearchResult {
28
+ companies: Company[];
29
+ total: number;
30
+ limit: number;
31
+ offset: number;
32
+ has_more: boolean;
33
+ }
34
+
35
+ export interface CompanySearchOptions extends SearchFilter {
36
+ include_people?: number; // 0..10; embed up to N people per company
37
+ }
38
+
39
+ export async function searchCompanies(apiKey: string, orgId: string, options: CompanySearchOptions): Promise<CompanySearchResult> {
40
+ return request('POST', `${BASE_URL}/company/orgs/${encodeURIComponent(orgId)}/search`, apiKey, options);
41
+ }
42
+
43
+ export async function getCompany(apiKey: string, orgId: string, companyId: string, include_people?: number): Promise<Company> {
44
+ const _url = include_people != null
45
+ ? `${BASE_URL}/company/orgs/${encodeURIComponent(orgId)}/${encodeURIComponent(companyId)}?include_people=${encodeURIComponent(String(include_people))}`
46
+ : `${BASE_URL}/company/orgs/${encodeURIComponent(orgId)}/${encodeURIComponent(companyId)}`;
47
+ return request('GET', _url, apiKey);
48
+ }
package/src/config.ts CHANGED
@@ -1,4 +1,15 @@
1
- export const HQ_BASE = process.env.MYAPI_HQ_URL ?? process.env.MYAPI_API_BASE ?? 'https://api.myapihq.com';
1
+ // Service base URLs. Today's reality: all primitives are served from the
2
+ // single `api.myapihq.com` gateway; per-service brand hosts
3
+ // (e.g. api.myllmapi.com) are aspirational and only some are wired in DNS
4
+ // + routed correctly. We default to the working hosts where they exist,
5
+ // and to the gateway where they don't (LLM, DATABASE, CRM, PEOPLE,
6
+ // COMPANY, AUDIENCE). The MYAPI_*_URL env vars stay as overrides for
7
+ // local dev — `source scripts/local-env.sh` still points everything at
8
+ // localhost:8080 regardless of the prod default.
9
+
10
+ const GATEWAY = 'https://api.myapihq.com';
11
+
12
+ export const HQ_BASE = process.env.MYAPI_HQ_URL ?? process.env.MYAPI_API_BASE ?? GATEWAY;
2
13
  export const DOMAIN_BASE = process.env.MYAPI_DOMAIN_URL ?? 'https://api.mydomainapi.com';
3
14
  export const FUNNEL_BASE = process.env.MYAPI_FUNNEL_URL ?? 'https://api.myfunnelapi.com';
4
15
  export const IMAGE_BASE = process.env.MYAPI_IMAGE_URL ?? 'https://api.myimageapi.com';
@@ -8,3 +19,12 @@ export const EMAIL_BASE = process.env.MYAPI_EMAIL_URL ?? 'https://api.myemai
8
19
  export const STORAGE_BASE = process.env.MYAPI_STORAGE_URL ?? 'https://api.mystorageapi.com';
9
20
  export const URL_BASE = process.env.MYAPI_URL_URL ?? 'https://api.myurlto.com';
10
21
  export const PIXEL_BASE = process.env.MYAPI_PIXEL_URL ?? 'https://api.mypixelapi.com';
22
+ // Per-service hosts below either don't resolve in DNS or 404 the routes;
23
+ // served via the gateway until backend wires the brand routing. Flip back
24
+ // to per-service hosts when DNS is sound and routes verified there.
25
+ export const PEOPLE_BASE = process.env.MYAPI_PEOPLE_URL ?? GATEWAY;
26
+ export const COMPANY_BASE = process.env.MYAPI_COMPANY_URL ?? GATEWAY;
27
+ export const AUDIENCE_BASE= process.env.MYAPI_AUDIENCE_URL?? GATEWAY;
28
+ export const LLM_BASE = process.env.MYAPI_LLM_URL ?? GATEWAY;
29
+ export const DATABASE_BASE= process.env.MYAPI_DATABASE_URL?? GATEWAY;
30
+ export const CRM_BASE = process.env.MYAPI_CRM_URL ?? GATEWAY;
package/src/crm.ts ADDED
@@ -0,0 +1,235 @@
1
+ import { request } from './client';
2
+ import { CRM_BASE as BASE_URL } from './config';
3
+ import type { Exposes } from './exposes';
4
+
5
+ export const EXPOSES: Exposes = [
6
+ 'POST /crm/orgs/{org_id}/contacts',
7
+ 'POST /crm/orgs/{org_id}/contacts/promote',
8
+ 'POST /crm/orgs/{org_id}/contacts/search',
9
+ 'GET /crm/orgs/{org_id}/contacts/{id}',
10
+ 'PATCH /crm/orgs/{org_id}/contacts/{id}',
11
+ 'DELETE /crm/orgs/{org_id}/contacts/{id}',
12
+ 'GET /crm/orgs/{org_id}/contacts/{id}/events',
13
+ 'POST /crm/orgs/{org_id}/companies',
14
+ 'POST /crm/orgs/{org_id}/companies/promote',
15
+ 'POST /crm/orgs/{org_id}/companies/search',
16
+ 'GET /crm/orgs/{org_id}/companies/{id}',
17
+ 'PATCH /crm/orgs/{org_id}/companies/{id}',
18
+ 'DELETE /crm/orgs/{org_id}/companies/{id}',
19
+ ];
20
+
21
+ // Shared enums — fixed sets. Lifecycle stage is the same enum for contacts
22
+ // and companies (Simon's call: keep it simple, easier for agents).
23
+ export type LifecycleStage =
24
+ | 'cold'
25
+ | 'warm'
26
+ | 'qualified'
27
+ | 'customer'
28
+ | 'churned';
29
+
30
+ export type ContactSource =
31
+ | 'goldfox'
32
+ | 'email'
33
+ | 'pixel'
34
+ | 'webhook'
35
+ | 'manual';
36
+
37
+ // Reserved set of event kinds. Agents cannot write these directly — they
38
+ // are emitted by the platform when state changes (stage_changed) or the
39
+ // underlying service completes (email_sent, webhook_received, etc.).
40
+ export type EventKind =
41
+ | 'created'
42
+ | 'promoted'
43
+ | 'stage_changed'
44
+ | 'email_sent'
45
+ | 'email_opened'
46
+ | 'email_clicked'
47
+ | 'email_replied'
48
+ | 'pixel_visit'
49
+ | 'webhook_received';
50
+
51
+ // Goldfox enrichment is a live join — the response embeds the current
52
+ // Goldfox row when goldfox_person_id is set. v1: returns null until the
53
+ // BQ get-by-id helper lands backend-side. The field shape will become
54
+ // the existing Person from people.ts once enabled.
55
+ export interface Contact {
56
+ id: string;
57
+ org_id: string;
58
+ email: string | null;
59
+ first_name: string | null;
60
+ last_name: string | null;
61
+ company_id: string | null;
62
+ lifecycle_stage: LifecycleStage;
63
+ custom: Record<string, unknown>;
64
+ source: ContactSource;
65
+ goldfox_person_id: string | null;
66
+ goldfox_person?: unknown | null; // deferred until BQ helper lands
67
+ created_at: string;
68
+ updated_at: string;
69
+ last_engagement_at?: string | null; // only set after engagement events (email/pixel/webhook)
70
+ deleted_at?: string | null; // soft-delete marker
71
+ }
72
+
73
+ export interface Company {
74
+ id: string;
75
+ org_id: string;
76
+ domain: string | null;
77
+ name: string | null;
78
+ lifecycle_stage: LifecycleStage;
79
+ custom: Record<string, unknown>;
80
+ source: ContactSource;
81
+ goldfox_company_id: string | null; // for Goldfox-sourced rows this equals the domain
82
+ goldfox_company?: unknown | null;
83
+ created_at: string;
84
+ updated_at: string;
85
+ deleted_at?: string | null;
86
+ }
87
+
88
+ export interface ContactEvent {
89
+ id: string;
90
+ contact_id: string;
91
+ kind: EventKind;
92
+ payload: Record<string, unknown>;
93
+ at: string;
94
+ }
95
+
96
+ // ── Inputs ────────────────────────────────────────────────────────────────
97
+
98
+ export interface CreateContactInput {
99
+ email?: string | null;
100
+ first_name?: string;
101
+ last_name?: string;
102
+ company_id?: string; // override the auto-link by domain
103
+ lifecycle_stage?: LifecycleStage;
104
+ custom?: Record<string, unknown>;
105
+ source?: ContactSource; // defaults to 'manual' server-side
106
+ }
107
+
108
+ export interface UpdateContactInput {
109
+ first_name?: string | null;
110
+ last_name?: string | null;
111
+ company_id?: string | null;
112
+ lifecycle_stage?: LifecycleStage;
113
+ custom?: Record<string, unknown>;
114
+ deleted_at?: null; // pass explicit null to restore a soft-deleted contact
115
+ }
116
+
117
+ export interface ContactSearchFilter {
118
+ lifecycle_stage?: LifecycleStage[];
119
+ source?: ContactSource[];
120
+ email?: string;
121
+ company_id?: string;
122
+ min_last_engagement_days?: number; // engaged within N days
123
+ max_last_engagement_days?: number; // last engaged > N days ago — re-engagement filter
124
+ include_deleted?: boolean; // default false
125
+ limit?: number;
126
+ offset?: number;
127
+ }
128
+
129
+ export interface ContactSearchResult {
130
+ contacts: Contact[];
131
+ total: number;
132
+ }
133
+
134
+ export interface CreateCompanyInput {
135
+ domain?: string | null;
136
+ name?: string;
137
+ lifecycle_stage?: LifecycleStage;
138
+ custom?: Record<string, unknown>;
139
+ source?: ContactSource;
140
+ }
141
+
142
+ export interface UpdateCompanyInput {
143
+ name?: string | null;
144
+ lifecycle_stage?: LifecycleStage;
145
+ custom?: Record<string, unknown>;
146
+ deleted_at?: null;
147
+ }
148
+
149
+ export interface CompanySearchFilter {
150
+ lifecycle_stage?: LifecycleStage[];
151
+ source?: ContactSource[];
152
+ domain?: string;
153
+ include_deleted?: boolean;
154
+ limit?: number;
155
+ offset?: number;
156
+ }
157
+
158
+ export interface CompanySearchResult {
159
+ companies: Company[];
160
+ total: number;
161
+ }
162
+
163
+ export interface EventsResponse {
164
+ events: ContactEvent[];
165
+ next_cursor?: string;
166
+ }
167
+
168
+ export interface ListEventsOptions {
169
+ limit?: number; // 1-200, default 50
170
+ cursor?: string;
171
+ kind?: EventKind;
172
+ }
173
+
174
+ // ── Contacts ──────────────────────────────────────────────────────────────
175
+
176
+ export async function createContact(apiKey: string, orgId: string, input: CreateContactInput): Promise<Contact> {
177
+ return request('POST', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts`, apiKey, input);
178
+ }
179
+
180
+ export async function getContact(apiKey: string, orgId: string, id: string): Promise<Contact> {
181
+ return request('GET', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts/${encodeURIComponent(id)}`, apiKey);
182
+ }
183
+
184
+ export async function updateContact(apiKey: string, orgId: string, id: string, patch: UpdateContactInput): Promise<Contact> {
185
+ return request('PATCH', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts/${encodeURIComponent(id)}`, apiKey, patch);
186
+ }
187
+
188
+ export async function deleteContact(apiKey: string, orgId: string, id: string): Promise<void> {
189
+ return request('DELETE', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts/${encodeURIComponent(id)}`, apiKey);
190
+ }
191
+
192
+ export async function searchContacts(apiKey: string, orgId: string, filter: ContactSearchFilter = {}): Promise<ContactSearchResult> {
193
+ return request('POST', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts/search`, apiKey, filter);
194
+ }
195
+
196
+ export async function promoteContact(apiKey: string, orgId: string, goldfoxPersonId: string): Promise<Contact> {
197
+ return request('POST', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts/promote`, apiKey, { goldfox_person_id: goldfoxPersonId });
198
+ }
199
+
200
+ export async function getContactEvents(apiKey: string, orgId: string, id: string, opts: ListEventsOptions = {}): Promise<EventsResponse> {
201
+ const qs = new URLSearchParams();
202
+ if (opts.limit != null) qs.append('limit', String(opts.limit));
203
+ if (opts.cursor != null) qs.append('cursor', opts.cursor);
204
+ if (opts.kind != null) qs.append('kind', opts.kind);
205
+ const q = qs.toString();
206
+ const url = `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/contacts/${encodeURIComponent(id)}/events${q ? `?${q}` : ''}`;
207
+ return request('GET', url, apiKey);
208
+ }
209
+
210
+ // ── Companies ─────────────────────────────────────────────────────────────
211
+
212
+ export async function createCompany(apiKey: string, orgId: string, input: CreateCompanyInput): Promise<Company> {
213
+ return request('POST', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/companies`, apiKey, input);
214
+ }
215
+
216
+ export async function getCompany(apiKey: string, orgId: string, id: string): Promise<Company> {
217
+ return request('GET', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/companies/${encodeURIComponent(id)}`, apiKey);
218
+ }
219
+
220
+ export async function updateCompany(apiKey: string, orgId: string, id: string, patch: UpdateCompanyInput): Promise<Company> {
221
+ return request('PATCH', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/companies/${encodeURIComponent(id)}`, apiKey, patch);
222
+ }
223
+
224
+ export async function deleteCompany(apiKey: string, orgId: string, id: string): Promise<void> {
225
+ return request('DELETE', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/companies/${encodeURIComponent(id)}`, apiKey);
226
+ }
227
+
228
+ export async function searchCompanies(apiKey: string, orgId: string, filter: CompanySearchFilter = {}): Promise<CompanySearchResult> {
229
+ return request('POST', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/companies/search`, apiKey, filter);
230
+ }
231
+
232
+ // Goldfox company id IS its domain (Simon's note in routes.md).
233
+ export async function promoteCompany(apiKey: string, orgId: string, domain: string): Promise<Company> {
234
+ return request('POST', `${BASE_URL}/crm/orgs/${encodeURIComponent(orgId)}/companies/promote`, apiKey, { goldfox_company_id: domain });
235
+ }
@@ -0,0 +1,103 @@
1
+ import { request } from './client';
2
+ import { DATABASE_BASE as BASE_URL } from './config';
3
+ import type { Exposes } from './exposes';
4
+
5
+ export const EXPOSES: Exposes = [
6
+ 'GET /database/orgs/{org_id}/namespaces',
7
+ 'POST /database/orgs/{org_id}/namespaces',
8
+ 'GET /database/orgs/{org_id}/namespaces/{ns}',
9
+ 'DELETE /database/orgs/{org_id}/namespaces/{ns}',
10
+ 'GET /database/orgs/{org_id}/namespaces/{ns}/keys',
11
+ 'GET /database/orgs/{org_id}/namespaces/{ns}/keys/{key}',
12
+ 'PUT /database/orgs/{org_id}/namespaces/{ns}/keys/{key}',
13
+ 'DELETE /database/orgs/{org_id}/namespaces/{ns}/keys/{key}',
14
+ ];
15
+
16
+ // Per-value JSON size cap. Mirrored server-side. Surfaced here so the CLI
17
+ // can fail fast before round-trip.
18
+ export const MAX_VALUE_BYTES = 256 * 1024;
19
+
20
+ export interface Namespace {
21
+ name: string;
22
+ org_id: string;
23
+ key_count: number; // approximate; eventually consistent
24
+ created_at: string;
25
+ }
26
+
27
+ export interface KeyEntry {
28
+ key: string;
29
+ value?: unknown; // present when values=true was requested or on get
30
+ etag: string;
31
+ updated_at: string;
32
+ }
33
+
34
+ export interface ListKeysOptions {
35
+ prefix?: string;
36
+ limit?: number; // 1-1000, default 50
37
+ cursor?: string; // opaque from previous response
38
+ values?: boolean; // default false
39
+ }
40
+
41
+ export interface ListKeysResponse {
42
+ keys: KeyEntry[];
43
+ next_cursor?: string; // present when more results exist
44
+ }
45
+
46
+ export interface ListNamespacesResponse {
47
+ namespaces: Namespace[];
48
+ }
49
+
50
+ export async function listNamespaces(apiKey: string, orgId: string): Promise<ListNamespacesResponse> {
51
+ return request('GET', `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces`, apiKey);
52
+ }
53
+
54
+ export async function createNamespace(apiKey: string, orgId: string, name: string): Promise<Namespace> {
55
+ return request('POST', `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces`, apiKey, { name });
56
+ }
57
+
58
+ export async function getNamespace(apiKey: string, orgId: string, name: string): Promise<Namespace> {
59
+ return request('GET', `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces/${encodeURIComponent(name)}`, apiKey);
60
+ }
61
+
62
+ export async function deleteNamespace(apiKey: string, orgId: string, name: string): Promise<void> {
63
+ return request('DELETE', `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces/${encodeURIComponent(name)}`, apiKey);
64
+ }
65
+
66
+ export async function listKeys(apiKey: string, orgId: string, ns: string, opts: ListKeysOptions = {}): Promise<ListKeysResponse> {
67
+ const qs = new URLSearchParams();
68
+ if (opts.prefix != null) qs.append('prefix', opts.prefix);
69
+ if (opts.limit != null) qs.append('limit', String(opts.limit));
70
+ if (opts.cursor != null) qs.append('cursor', opts.cursor);
71
+ if (opts.values) qs.append('values', 'true');
72
+ const q = qs.toString();
73
+ const url = `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces/${encodeURIComponent(ns)}/keys${q ? `?${q}` : ''}`;
74
+ return request('GET', url, apiKey);
75
+ }
76
+
77
+ export async function getKey(apiKey: string, orgId: string, ns: string, key: string): Promise<KeyEntry> {
78
+ return request('GET', `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces/${encodeURIComponent(ns)}/keys/${encodeURIComponent(key)}`, apiKey);
79
+ }
80
+
81
+ // `ifMatch` enables compare-and-swap: server returns 412 ETAG_MISMATCH when
82
+ // the stored etag differs. Omit for last-write-wins.
83
+ export async function putKey(apiKey: string, orgId: string, ns: string, key: string, value: unknown, ifMatch?: string): Promise<KeyEntry> {
84
+ const headers = ifMatch ? { 'If-Match': ifMatch } : undefined;
85
+ return request(
86
+ 'PUT',
87
+ `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces/${encodeURIComponent(ns)}/keys/${encodeURIComponent(key)}`,
88
+ apiKey,
89
+ { value },
90
+ headers,
91
+ );
92
+ }
93
+
94
+ export async function deleteKey(apiKey: string, orgId: string, ns: string, key: string, ifMatch?: string): Promise<void> {
95
+ const headers = ifMatch ? { 'If-Match': ifMatch } : undefined;
96
+ return request(
97
+ 'DELETE',
98
+ `${BASE_URL}/database/orgs/${encodeURIComponent(orgId)}/namespaces/${encodeURIComponent(ns)}/keys/${encodeURIComponent(key)}`,
99
+ apiKey,
100
+ undefined,
101
+ headers,
102
+ );
103
+ }
package/src/domain.ts CHANGED
@@ -1,5 +1,18 @@
1
1
  import { request } from './client';
2
2
  import { DOMAIN_BASE as BASE_URL } from './config';
3
+ import type { Exposes } from './exposes';
4
+
5
+ export const EXPOSES: Exposes = [
6
+ 'GET /domain/orgs/{org_id}/list',
7
+ 'GET /domain/orgs/{org_id}/check/available/{name}',
8
+ 'POST /domain/orgs/{org_id}/register',
9
+ 'POST /domain/orgs/{org_id}/import',
10
+ 'POST /domain/orgs/{org_id}/{domain}/assign',
11
+ 'POST /domain/orgs/{org_id}/{domain}/renew',
12
+ 'GET /domain/orgs/{org_id}/{domain}/settings',
13
+ 'POST /domain/orgs/{org_id}/{domain}/settings',
14
+ 'GET /domain/orgs/{org_id}/{domain}/status',
15
+ ];
3
16
 
4
17
 
5
18
 
@@ -21,12 +34,41 @@ export interface DomainSettings {
21
34
  is_robots_txt_managed: boolean;
22
35
  }
23
36
 
37
+ // ICANN-required WHOIS contact info for domain registration. Sent in the
38
+ // register-domain request body when REGISTRAR_PROVIDER=cloudflare is set
39
+ // server-side; ignored (but accepted) under the legacy OpenProvider path.
40
+ // The CF account holds the zone; this is the WHOIS registrant.
41
+ export interface Registrant {
42
+ name: string;
43
+ email: string;
44
+ phone: string; // E.164 format recommended (e.g. "+33612345678")
45
+ street: string;
46
+ city: string;
47
+ state?: string; // Required for US / CA registrants; optional elsewhere.
48
+ postal_code: string;
49
+ country_code: string; // ISO 3166-1 alpha-2 (e.g. "US", "DE", "FR")
50
+ organization?: string; // Optional organization name (WHOIS "Registrant Organization" field).
51
+ }
52
+
53
+ export interface RegisterDomainInput {
54
+ years?: number;
55
+ registrant: Registrant;
56
+ }
57
+
24
58
  export async function checkDomain(apiKey: string, orgId: string, domain: string): Promise<{ available: boolean; price_cents: number }> {
25
59
  return request('GET', `${BASE_URL}/domain/orgs/${encodeURIComponent(orgId)}/check/available/${encodeURIComponent(domain)}`, apiKey);
26
60
  }
27
61
 
28
- export async function registerDomain(apiKey: string, orgId: string, domain: string, years?: number): Promise<DomainRecord> {
29
- return request('POST', `${BASE_URL}/domain/orgs/${encodeURIComponent(orgId)}/register`, apiKey, { domain, years });
62
+ export async function registerDomain(apiKey: string, orgId: string, domain: string, input: RegisterDomainInput): Promise<DomainRecord> {
63
+ return request('POST', `${BASE_URL}/domain/orgs/${encodeURIComponent(orgId)}/register`, apiKey, {
64
+ domain,
65
+ years: input.years,
66
+ registrant: input.registrant,
67
+ });
68
+ }
69
+
70
+ export async function renewDomain(apiKey: string, orgId: string, domain: string): Promise<DomainRecord> {
71
+ return request('POST', `${BASE_URL}/domain/orgs/${encodeURIComponent(orgId)}/${encodeURIComponent(domain)}/renew`, apiKey);
30
72
  }
31
73
 
32
74
  export async function importDomain(apiKey: string, orgId: string, payload: { domain: string; namecheap_api_user?: string; namecheap_api_key?: string; }): Promise<DomainRecord> {
package/src/email.ts CHANGED
@@ -1,5 +1,49 @@
1
1
  import { request } from './client';
2
2
  import { EMAIL_BASE as BASE_URL } from './config';
3
+ import type { Exposes } from './exposes';
4
+
5
+ export const EXPOSES: Exposes = [
6
+ // Mailbox / sending activation
7
+ 'POST /email/mailboxes/create',
8
+ 'GET /email/mailboxes',
9
+ 'POST /email/sending/activate',
10
+ // Send + read
11
+ 'POST /email/send',
12
+ 'GET /email/status/{message_id}',
13
+ 'GET /email/sent',
14
+ 'GET /email/inbox/{address}',
15
+ 'GET /email/outbox/{address}',
16
+ 'GET /email/message/{message_id}',
17
+ // Warmup
18
+ 'POST /email/warmup/start',
19
+ 'POST /email/warmup/pause',
20
+ 'POST /email/warmup/resume',
21
+ 'POST /email/warmup/stop',
22
+ 'GET /email/warmup/stats/{address}',
23
+ // Templates
24
+ 'POST /email/orgs/{org_id}/templates/generate',
25
+ 'GET /email/orgs/{org_id}/template-jobs/{job_id}',
26
+ 'GET /email/orgs/{org_id}/templates',
27
+ 'GET /email/orgs/{org_id}/templates/{template_id}',
28
+ 'POST /email/orgs/{org_id}/templates/{template_id}/edit',
29
+ 'POST /email/orgs/{org_id}/templates/{template_id}/send-test',
30
+ 'DELETE /email/orgs/{org_id}/templates/{template_id}',
31
+ 'GET /email/templates/{template_id}/preview',
32
+ // Campaigns
33
+ 'POST /email/orgs/{org_id}/campaigns',
34
+ 'GET /email/orgs/{org_id}/campaigns',
35
+ 'GET /email/orgs/{org_id}/campaigns/{campaign_id}',
36
+ 'PATCH /email/orgs/{org_id}/campaigns/{campaign_id}',
37
+ 'POST /email/orgs/{org_id}/campaigns/{campaign_id}/contacts/upload-list',
38
+ 'POST /email/orgs/{org_id}/campaigns/{campaign_id}/contacts/upload-file',
39
+ 'GET /email/orgs/{org_id}/campaigns/{campaign_id}/contacts/status',
40
+ 'POST /email/orgs/{org_id}/campaigns/{campaign_id}/start',
41
+ 'POST /email/orgs/{org_id}/campaigns/{campaign_id}/pause',
42
+ 'POST /email/orgs/{org_id}/campaigns/{campaign_id}/resume',
43
+ 'GET /email/orgs/{org_id}/campaigns/{campaign_id}/stats',
44
+ // Verify
45
+ 'POST /email/orgs/{org_id}/verify',
46
+ ];
3
47
 
4
48
  export interface EmailMessage { message_id: string; from: string; subject: string; body?: string; received_at: string }
5
49
  export interface EmailTemplate { id: string; name: string; subject: string; preview_url: string; created_at: string; updated_at: string }
@@ -12,6 +56,40 @@ export interface CampaignStats {
12
56
  engagement: { sent: number; opens: number; clicks: number; page_visits: number };
13
57
  }
14
58
 
59
+ // ── Email verification ──────────────────────────────────────────────────────
60
+ //
61
+ // Synchronous single-address verification — syntax + DNS + a Microsoft
62
+ // GetCredentialType probe. Cheap layer only: ~50% of inputs return a
63
+ // definitive verdict in <1s; the rest get verdict='unknown' with
64
+ // smtp_recommended=true, suggesting a downstream SMTP probe (not in this
65
+ // API today).
66
+
67
+ export type VerifyVerdict = 'deliverable' | 'undeliverable' | 'unknown';
68
+
69
+ export interface VerifyChecks {
70
+ syntax: { valid: boolean; detail?: string };
71
+ dns?: { valid: boolean; mx_records?: string[] };
72
+ microsoft?: {
73
+ verdict: string; // e.g. 'federated_unknown', 'not_microsoft_domain'
74
+ if_exists_result: number; // Microsoft GetCredentialType enum
75
+ domain_type: number;
76
+ federated: boolean;
77
+ };
78
+ }
79
+
80
+ export interface VerifyResult {
81
+ email: string;
82
+ verdict: VerifyVerdict;
83
+ confidence: number; // 0..1
84
+ smtp_recommended: boolean;
85
+ checks: VerifyChecks;
86
+ elapsed_ms: number;
87
+ }
88
+
89
+ export async function verifyEmail(apiKey: string, orgId: string, email: string): Promise<VerifyResult> {
90
+ return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/verify`, apiKey, { email });
91
+ }
92
+
15
93
  // ── Mailbox ops (account-scoped) ─────────────────────────────────────────────
16
94
 
17
95
  export async function createMailbox(apiKey: string, domain: string, username: string, displayName?: string): Promise<{ address: string; created_at: string }> {
@@ -96,6 +174,12 @@ export async function getTemplate(apiKey: string, orgId: string, templateId: str
96
174
  return request('GET', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}`, apiKey);
97
175
  }
98
176
 
177
+ // Public preview — no auth required. The endpoint is unauthenticated per
178
+ // the OpenAPI schema; access is gated by the unguessable template_id.
179
+ export async function getTemplatePreview(templateId: string): Promise<{ html?: string; subject?: string; preview_url?: string } & Record<string, unknown>> {
180
+ return request('GET', `${BASE_URL}/email/templates/${encodeURIComponent(templateId)}/preview`, undefined);
181
+ }
182
+
99
183
  export async function editTemplate(apiKey: string, orgId: string, templateId: string, prompt: string): Promise<{ template_id: string; preview_url: string }> {
100
184
  return request('POST', `${BASE_URL}/email/orgs/${encodeURIComponent(orgId)}/templates/${encodeURIComponent(templateId)}/edit`, apiKey, { prompt });
101
185
  }
package/src/exposes.ts ADDED
@@ -0,0 +1,16 @@
1
+ // OpenAPI-templated endpoint declaration for SDK service modules.
2
+ //
3
+ // Each service module (hq, domain, funnel, ...) exports an `EXPOSES: Exposes`
4
+ // array listing the backend endpoints reachable through that module's
5
+ // functions. The integrations coverage tool walks these to verify CLI ↔ SDK
6
+ // parity (S-108).
7
+ //
8
+ // Form: `<METHOD> <PATH>` where path uses `{name}` for variable segments
9
+ // matching OpenAPI 3.x style.
10
+ //
11
+ // Mirrors packages/cli/src/exposes.ts — kept duplicated to avoid runtime
12
+ // dependencies between the packages (the SDK has zero runtime deps per NFR-005).
13
+
14
+ export type Endpoint = `${'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'} /${string}`;
15
+
16
+ export type Exposes = readonly Endpoint[];
package/src/funnel.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import { request } from './client';
2
2
  import { FUNNEL_BASE as BASE_URL } from './config';
3
+ import type { Exposes } from './exposes';
4
+
5
+ export const EXPOSES: Exposes = [
6
+ 'POST /funnel/orgs/{org_id}/funnels',
7
+ 'GET /funnel/orgs/{org_id}/funnels',
8
+ 'GET /funnel/orgs/{org_id}/funnels/{funnel_id}',
9
+ 'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}',
10
+ 'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/push-page',
11
+ 'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
12
+ 'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
13
+ ];
3
14
 
4
15
 
5
16
 
@@ -27,6 +38,29 @@ export async function deleteFunnel(apiKey: string, orgId: string, funnelId: stri
27
38
  export async function pushFunnelPage(apiKey: string, orgId: string, funnelId: string, payload: { slug: string; html: string }): Promise<{ url: string }> {
28
39
  return request('POST', `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/push-page`, apiKey, payload);
29
40
  }
41
+
42
+ export interface FunnelPage {
43
+ slug: string;
44
+ url?: string;
45
+ published_at?: string;
46
+ updated_at?: string;
47
+ // Backend may include other fields; we type the common ones and let the
48
+ // rest pass through for printJson() / printTable() consumers.
49
+ [key: string]: unknown;
50
+ }
51
+
52
+ export async function listFunnelPages(apiKey: string, orgId: string, funnelId: string): Promise<FunnelPage[]> {
53
+ // Backend returns `{ pages: [...] }` for this endpoint (every other list
54
+ // endpoint returns a flat array). Unwrap so the SDK's contract matches the
55
+ // rest — callers expect an array.
56
+ const res = await request<{ pages?: FunnelPage[] } | FunnelPage[]>(
57
+ 'GET',
58
+ `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/pages`,
59
+ apiKey,
60
+ );
61
+ if (Array.isArray(res)) return res;
62
+ return res?.pages ?? [];
63
+ }
30
64
  export async function verifyFunnel(apiKey: string, orgId: string, funnelId: string, opts?: { html?: string; slug?: string }): Promise<VerifyResult> {
31
65
  return request('POST', `${BASE_URL}/funnel/orgs/${encodeURIComponent(orgId)}/funnels/${encodeURIComponent(funnelId)}/verify`, apiKey, opts || {});
32
66
  }