@myapihq/sdk 2.4.0 → 2.4.2

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.
@@ -1,501 +0,0 @@
1
- #!/usr/bin/env node
2
- // SDK scaffolder. NOT a fully-autonomous codegen — it's a schema-driven
3
- // boilerplate emitter where you (the human) choose function names, type
4
- // aliases, and response shapes; the schema is the truth source for which
5
- // endpoints exist, their methods, paths, path params, and request bodies.
6
- //
7
- // Usage:
8
- // node scripts/scaffold-sdk.js ← scaffold every service in config
9
- // node scripts/scaffold-sdk.js --service=people ← scaffold one service
10
- // node scripts/scaffold-sdk.js --dry-run ← print what would be written
11
- //
12
- // Why scaffolding, not full codegen:
13
- // The existing hand-written SDK modules (webhook, funnel, email, ...) embed
14
- // real UX choices: `createEndpoint` not `postWebhookOrgsEndpoints`; `Delivery`
15
- // not `WebhookDeliveryResponse`. A full codegen would either produce a worse
16
- // surface (robot names) or require so much config to recover the hand-written
17
- // shape that you might as well hand-write it. The middle ground: humans
18
- // pick names/types; the script enforces the schema contract and emits the
19
- // boilerplate function bodies + EXPOSES arrays.
20
- //
21
- // Future services scaffold cleanly: add an entry to SERVICES below, run the
22
- // script, get a new file. Drift detection: re-running on a service whose
23
- // schema endpoints changed will flag the diff before writing.
24
-
25
- import * as fs from 'node:fs';
26
- import * as path from 'node:path';
27
- import { fileURLToPath } from 'node:url';
28
-
29
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
- const PKG_ROOT = path.resolve(__dirname, '..');
31
- const SCHEMA_PATH = path.resolve(PKG_ROOT, '..', 'cli', 'schema-snapshot.json');
32
- const OUT_DIR = path.resolve(PKG_ROOT, 'src');
33
-
34
- // ─────────────────────────────────────────────────────────────────────────────
35
- // Service configs
36
- // ─────────────────────────────────────────────────────────────────────────────
37
- //
38
- // Each entry describes one SDK module. The script generates ONE file per entry
39
- // at packages/sdk/src/<service>.ts. Add new services by appending to this list
40
- // and re-running.
41
-
42
- const SERVICES = [
43
- {
44
- service: 'people',
45
- baseConst: 'PEOPLE_BASE',
46
- sharedImports: [`import type { SearchFilter } from './audience';`],
47
- types: `
48
- export interface Location {
49
- city?: string;
50
- region?: string;
51
- country?: string; // ISO 3166-1 alpha-2
52
- }
53
-
54
- // Goldfox-sourced person row. The data model is crawl-derived signals over
55
- // scraped contact pages; rich on quality tiers and behavioral booleans,
56
- // thin on legacy CRM fields (no title / function / industry as response
57
- // columns — many of those moved to filterable inputs only).
58
- export interface Person {
59
- id: string;
60
- full_name: string;
61
- first_name?: string;
62
- last_name?: string;
63
- email?: string;
64
- email_type?: string; // corporate | freemail | role_based | other_corporate
65
- link_confidence?: number; // 0..1 — 1.0 = email-domain match (definitive)
66
- location: Location;
67
- company_id: string;
68
- company: PersonCompany;
69
- }
70
-
71
- // Person.company embeds the same shape returned by /company/search results.
72
- // PersonCompany is an alias for Company — the company sub-object on a person
73
- // row is identical to a standalone company row.
74
- export interface PersonCompany {
75
- id: string;
76
- domain?: string;
77
- name: string;
78
- general_phone?: string;
79
- address?: string;
80
- is_registered_entity: boolean;
81
- confidence: 'high' | 'low' | 'very_low';
82
- country?: string;
83
- country_consistent: boolean;
84
- tld_class?: string; // cctld | generic | vanity | low_trust | other
85
- has_careers_page: boolean;
86
- has_investors_page: boolean;
87
- has_shop_page: boolean;
88
- has_blog: boolean;
89
- has_c_level: boolean;
90
- has_decision_maker: boolean;
91
- headcount_lower_bound: number;
92
- org_breadth: number;
93
- source_count: number;
94
- subdomain_variety: number;
95
- multilingual: boolean;
96
- language_count: number;
97
- location?: Location;
98
- }
99
-
100
- export interface PeopleSearchResult {
101
- people: Person[];
102
- total: number;
103
- limit: number;
104
- offset: number;
105
- has_more: boolean;
106
- }`,
107
- operations: [
108
- { name: 'searchPeople', method: 'POST', path: '/people/orgs/{org_id}/search', body: 'filter: SearchFilter', returns: 'PeopleSearchResult' },
109
- { name: 'getPerson', method: 'GET', path: '/people/orgs/{org_id}/{person_id}', returns: 'Person' },
110
- ],
111
- },
112
-
113
- {
114
- service: 'company',
115
- baseConst: 'COMPANY_BASE',
116
- sharedImports: [
117
- `import type { SearchFilter } from './audience';`,
118
- `import type { Person } from './people';`,
119
- ],
120
- types: `
121
- import type { Location, PersonCompany } from './people';
122
-
123
- // Company is the same shape as PersonCompany (a person's embedded company)
124
- // with an optional embedded people array when include_people=N is passed.
125
- export interface Company extends PersonCompany {
126
- people?: Person[]; // present when include_people=N is passed
127
- }
128
-
129
- export interface CompanySearchResult {
130
- companies: Company[];
131
- total: number;
132
- limit: number;
133
- offset: number;
134
- has_more: boolean;
135
- }
136
-
137
- export interface CompanySearchOptions extends SearchFilter {
138
- include_people?: number; // 0..10; embed up to N people per company
139
- }`,
140
- operations: [
141
- { name: 'searchCompanies', method: 'POST', path: '/company/orgs/{org_id}/search', body: 'options: CompanySearchOptions', returns: 'CompanySearchResult' },
142
- { name: 'getCompany', method: 'GET', path: '/company/orgs/{org_id}/{company_id}', query: 'include_people?: number', returns: 'Company' },
143
- ],
144
- },
145
-
146
- {
147
- service: 'audience',
148
- baseConst: 'AUDIENCE_BASE',
149
- sharedImports: [
150
- `import type { Person } from './people';`,
151
- `import type { Company } from './company';`,
152
- ],
153
- types: `
154
- // Canonical filter shape — shared across /people/search, /company/search,
155
- // and embedded in saved audiences. Goldfox-sourced; OR within array, AND
156
- // across fields. Empty filter = all rows in source with default high-only
157
- // confidence.
158
- //
159
- // Some fields are people-source-only (seniority, email_type,
160
- // min_link_confidence) and silently ignored by company-source contexts.
161
- export interface SearchFilter {
162
- // Confidence tier(s) — high (default, 96.4% of dataset, no UGC),
163
- // low (UGC source — spot-check), very_low (UGC + freemail mismatch).
164
- // Pass explicit array to widen beyond the default high-only.
165
- confidence?: ('high' | 'low' | 'very_low')[];
166
-
167
- // ISO 3166-1 alpha-2 country code(s). Consensus across TLD + address + phone.
168
- country?: string[];
169
-
170
- // Only rows where TLD + address + phone all agree on country.
171
- country_consistent?: boolean;
172
-
173
- // Email_type — corporate / freemail / role_based / other_corporate.
174
- // (Ignored on /company/search.)
175
- email_type?: ('corporate' | 'freemail' | 'role_based' | 'other_corporate')[];
176
-
177
- // Behavioral booleans — derived from page crawl signals.
178
- has_c_level?: boolean; // ≥1 person with seniority = c_level
179
- has_careers_page?: boolean; // /careers, /jobs, etc. — growth-stage proxy
180
- has_decision_maker?: boolean; // ≥1 person with seniority ∈ (c_level, vp_director)
181
- has_investors_page?: boolean; // /investors, /ir/ — mature-org proxy
182
- has_shop_page?: boolean; // /shop, /store, /products — e-commerce proxy
183
-
184
- // companies[0].legal_form_country is not null — name has a recognised
185
- // legal suffix (GmbH/Ltd/Inc/SARL/...).
186
- is_registered_entity?: boolean;
187
-
188
- // Case-insensitive substring match on the row's domain.
189
- keyword?: string;
190
-
191
- // Pagination
192
- limit?: number; // default 20, max 100
193
- offset?: number;
194
-
195
- // Row's headcount_lower_bound (≈ ARRAY_LENGTH(people)) must be ≥ this.
196
- min_headcount?: number;
197
-
198
- // Drop people whose link_confidence is below this threshold (0–1).
199
- // 1.0 = email-domain match (definitive); 0.5 = strong; lower = weak.
200
- // (Ignored on /company/search.)
201
- min_link_confidence?: number;
202
-
203
- // Row must appear in at least this many distinct source URLs.
204
- min_source_count?: number;
205
-
206
- // Goldfox seniority — c_level, vp_director, manager, senior_ic, ic.
207
- // (Ignored on /company/search.)
208
- seniority?: ('c_level' | 'vp_director' | 'manager' | 'senior_ic' | 'ic')[];
209
-
210
- // TLD class — cctld / generic / vanity / low_trust / other.
211
- tld_class?: ('cctld' | 'generic' | 'vanity' | 'low_trust' | 'other')[];
212
- }
213
-
214
- export type AudienceSource = 'people' | 'company';
215
-
216
- export interface Audience {
217
- id: string;
218
- org_id: string;
219
- name: string;
220
- description?: string;
221
- source: AudienceSource;
222
- filter: SearchFilter;
223
- member_count: number;
224
- created_at: string;
225
- updated_at: string;
226
- }
227
-
228
- // Backend returns 'people' or 'companies' keyed by source; the response
229
- // itself does NOT carry the source field, so callers either know it from
230
- // the audience object or detect by which array is non-empty.
231
- export interface AudienceMembers {
232
- total: number;
233
- limit: number;
234
- offset: number;
235
- has_more: boolean;
236
- people?: Person[];
237
- companies?: Company[];
238
- }
239
-
240
- export interface AudienceRefreshResult {
241
- total: number; // new member count
242
- previous: number; // count before refresh
243
- delta: number; // total - previous
244
- refreshed_at: string;
245
- }
246
-
247
- export interface CreateAudienceInput {
248
- name: string;
249
- source: AudienceSource;
250
- filter: SearchFilter;
251
- description?: string;
252
- }
253
-
254
- export interface UpdateAudienceInput {
255
- name?: string;
256
- description?: string;
257
- filter?: SearchFilter;
258
- }`,
259
- operations: [
260
- { name: 'createAudience', method: 'POST', path: '/audience/orgs/{org_id}/audiences', body: 'input: CreateAudienceInput', returns: 'Audience' },
261
- { name: 'listAudiences', method: 'GET', path: '/audience/orgs/{org_id}/audiences', returns: 'Audience[]' },
262
- { name: 'getAudience', method: 'GET', path: '/audience/orgs/{org_id}/audiences/{audience_id}', returns: 'Audience' },
263
- { name: 'updateAudience', method: 'PATCH', path: '/audience/orgs/{org_id}/audiences/{audience_id}', body: 'patch: UpdateAudienceInput', returns: 'Audience' },
264
- { name: 'deleteAudience', method: 'DELETE', path: '/audience/orgs/{org_id}/audiences/{audience_id}', returns: 'void' },
265
- { name: 'getAudienceMembers', method: 'GET', path: '/audience/orgs/{org_id}/audiences/{audience_id}/members', query: 'limit?: number; offset?: number', returns: 'AudienceMembers' },
266
- { name: 'refreshAudience', method: 'POST', path: '/audience/orgs/{org_id}/audiences/{audience_id}/refresh', returns: 'AudienceRefreshResult' },
267
- ],
268
- },
269
-
270
- {
271
- service: 'llm',
272
- baseConst: 'LLM_BASE',
273
- types: `
274
- export type Role = 'system' | 'user' | 'assistant';
275
-
276
- export interface Message {
277
- role: Role;
278
- content: string;
279
- }
280
-
281
- export interface CompleteRequest {
282
- model: string;
283
- messages: Message[];
284
- max_tokens?: number;
285
- temperature?: number;
286
- stop?: string[];
287
- }
288
-
289
- export interface CompleteResponse {
290
- model: string;
291
- content: string;
292
- finish_reason: string; // 'stop' | 'length' | 'safety' | ...
293
- usage: {
294
- input_tokens: number;
295
- output_tokens: number;
296
- cost_usd: number;
297
- };
298
- }
299
-
300
- export interface EmbedRequest {
301
- model: string;
302
- input: string | string[]; // single string or batch
303
- }
304
-
305
- // Always returned as an array — even for a single-string input you get a
306
- // one-element array. Each element is the dense vector as a flat number[].
307
- // Embed-only usage has no output_tokens.
308
- export interface EmbedResponse {
309
- model: string;
310
- embeddings: number[][];
311
- usage: {
312
- input_tokens: number;
313
- cost_usd: number;
314
- };
315
- }
316
-
317
- // Catalog row. 'chat' models have context + output_per_1m; 'embed' models
318
- // have dimensions instead. Pricing is per 1M tokens at upstream rates
319
- // (we resell at-cost while building our own inference).
320
- export interface Model {
321
- id: string;
322
- kind: 'chat' | 'embed';
323
- context?: number;
324
- dimensions?: number;
325
- input_per_1m: number;
326
- output_per_1m?: number;
327
- }
328
-
329
- export interface ModelsResponse {
330
- models: Model[];
331
- }`,
332
- operations: [
333
- { name: 'complete', method: 'POST', path: '/llm/orgs/{org_id}/complete', body: 'req: CompleteRequest', returns: 'CompleteResponse' },
334
- { name: 'embed', method: 'POST', path: '/llm/orgs/{org_id}/embed', body: 'req: EmbedRequest', returns: 'EmbedResponse' },
335
- { name: 'listModels', method: 'GET', path: '/llm/orgs/{org_id}/models', returns: 'ModelsResponse' },
336
- ],
337
- },
338
-
339
- ];
340
-
341
- // ─────────────────────────────────────────────────────────────────────────────
342
- // Generator
343
- // ─────────────────────────────────────────────────────────────────────────────
344
-
345
- function parseArgs(argv) {
346
- const out = { service: null, dryRun: false, help: false };
347
- for (const a of argv) {
348
- if (a === '--help' || a === '-h') out.help = true;
349
- else if (a === '--dry-run') out.dryRun = true;
350
- else if (a.startsWith('--service=')) out.service = a.slice('--service='.length);
351
- }
352
- return out;
353
- }
354
-
355
- function loadSchema() {
356
- if (!fs.existsSync(SCHEMA_PATH)) {
357
- throw new Error(`Schema snapshot not found at ${SCHEMA_PATH}. Run: cd packages/cli && npm run update-schema`);
358
- }
359
- return JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf-8'));
360
- }
361
-
362
- // Path params: /a/{x}/b/{y} → ['x', 'y']
363
- function extractPathParams(p) {
364
- const out = [];
365
- const re = /\{([^}]+)\}/g;
366
- let m;
367
- while ((m = re.exec(p)) !== null) out.push(m[1]);
368
- return out;
369
- }
370
-
371
- // snake_case → camelCase, used for path-param variable names + arg names.
372
- function snakeToCamel(s) {
373
- return s.replace(/_(.)/g, (_, c) => c.toUpperCase());
374
- }
375
-
376
- // Build the function body with proper interpolated path and request(...) call.
377
- function buildFunction(op, baseConst) {
378
- const pathParams = extractPathParams(op.path);
379
- // org_id is always the first path param by convention; surface as orgId for the function arg.
380
- const argList = ['apiKey: string'];
381
- const pathExprParts = []; // pieces to concatenate inside the template literal
382
-
383
- // Walk the path and build the template literal substituting {param} → ${encodeURIComponent(varName)}
384
- let interpolated = op.path.replace(/\{([^}]+)\}/g, (_, name) => {
385
- const camel = snakeToCamel(name);
386
- return '${encodeURIComponent(' + camel + ')}';
387
- });
388
-
389
- // Function args, in path-param order
390
- for (const p of pathParams) argList.push(`${snakeToCamel(p)}: string`);
391
-
392
- // Body (if any)
393
- if (op.body) argList.push(op.body);
394
-
395
- // Query (if any) — keep distinct from body
396
- if (op.query) argList.push(op.query.split(';').length === 1
397
- ? op.query
398
- : `opts: { ${op.query} }`);
399
-
400
- // Build query string segment if needed
401
- let urlExpr = '`' + '${' + baseConst + '}' + interpolated + '`';
402
- let querySerializer = '';
403
- if (op.query) {
404
- // Simple shape — only handles `name?: type` (one param) or `{ a?: t; b?: t }` (multi)
405
- const isMulti = op.query.includes(';');
406
- if (isMulti) {
407
- querySerializer = `
408
- const _q = new URLSearchParams();
409
- for (const [k, v] of Object.entries(opts)) {
410
- if (v != null) _q.append(k, String(v));
411
- }
412
- const _qs = _q.toString();
413
- const _url = _qs ? \`${urlExpr.slice(1, -1)}?\${_qs}\` : ${urlExpr};`;
414
- urlExpr = '_url';
415
- } else {
416
- // Single optional query param: name?: type
417
- const paramName = op.query.replace(/[?:].*/, '').trim();
418
- querySerializer = `
419
- const _url = ${paramName} != null
420
- ? \`${urlExpr.slice(1, -1)}?${paramName}=\${encodeURIComponent(String(${paramName}))}\`
421
- : ${urlExpr};`;
422
- urlExpr = '_url';
423
- }
424
- }
425
-
426
- const bodyArg = op.body ? `, ${op.body.split(':')[0].trim()}` : '';
427
- const returnsVoid = op.returns === 'void';
428
-
429
- const sig = `export async function ${op.name}(${argList.join(', ')}): Promise<${op.returns}> {`;
430
- const body = [
431
- querySerializer,
432
- ` return request('${op.method}', ${urlExpr}, apiKey${bodyArg});`,
433
- ].filter(Boolean).join('\n').replace(/^\n/, '');
434
-
435
- return `${sig}\n${body}\n}`;
436
- }
437
-
438
- function renderModule(svc, schema) {
439
- // Validate each operation's endpoint exists in the schema (drift detection).
440
- for (const op of svc.operations) {
441
- const pathItem = schema.paths[op.path];
442
- if (!pathItem) throw new Error(`${svc.service}: schema has no path "${op.path}"`);
443
- if (!pathItem[op.method.toLowerCase()]) {
444
- throw new Error(`${svc.service}: schema path "${op.path}" has no method ${op.method}`);
445
- }
446
- }
447
-
448
- const exposes = svc.operations.map(op => `'${op.method} ${op.path}'`).join(',\n ');
449
- const functions = svc.operations.map(op => buildFunction(op, 'BASE_URL')).join('\n\n');
450
- const sharedImports = (svc.sharedImports || []).join('\n');
451
-
452
- return `// Scaffolded from schema by scripts/scaffold-sdk.js — DO NOT remove the
453
- // EXPOSES array (the coverage tool verifies it against the live schema).
454
- // Types and function names ARE editable; re-running the scaffolder against
455
- // a service whose config has been changed will diff vs. existing file
456
- // (use --dry-run to inspect).
457
-
458
- import { request } from './client';
459
- import { ${svc.baseConst} as BASE_URL } from './config';
460
- import type { Exposes } from './exposes';
461
- ${sharedImports ? '\n' + sharedImports + '\n' : ''}
462
- export const EXPOSES: Exposes = [
463
- ${exposes},
464
- ];
465
- ${svc.types}
466
-
467
- ${functions}
468
- `;
469
- }
470
-
471
- function main() {
472
- const args = parseArgs(process.argv.slice(2));
473
- if (args.help) {
474
- console.log(`Usage: scaffold-sdk [--service=<name>] [--dry-run]`);
475
- process.exit(0);
476
- }
477
-
478
- const schema = loadSchema();
479
- const services = args.service
480
- ? SERVICES.filter(s => s.service === args.service)
481
- : SERVICES;
482
-
483
- if (services.length === 0) {
484
- console.error(`Unknown service: ${args.service}. Known: ${SERVICES.map(s => s.service).join(', ')}`);
485
- process.exit(1);
486
- }
487
-
488
- for (const svc of services) {
489
- const out = renderModule(svc, schema);
490
- const file = path.join(OUT_DIR, `${svc.service}.ts`);
491
- if (args.dryRun) {
492
- console.log(`──── ${file} ────`);
493
- console.log(out);
494
- } else {
495
- fs.writeFileSync(file, out, 'utf-8');
496
- console.log(`✓ wrote ${path.relative(process.cwd(), file)} (${svc.operations.length} operations)`);
497
- }
498
- }
499
- }
500
-
501
- main();
package/src/audience.ts DELETED
@@ -1,162 +0,0 @@
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 { AUDIENCE_BASE as BASE_URL } from './config';
9
- import type { Exposes } from './exposes';
10
-
11
- import type { Person } from './people';
12
- import type { Company } from './company';
13
-
14
- export const EXPOSES: Exposes = [
15
- 'POST /audience/orgs/{org_id}/audiences',
16
- 'GET /audience/orgs/{org_id}/audiences',
17
- 'GET /audience/orgs/{org_id}/audiences/{audience_id}',
18
- 'PATCH /audience/orgs/{org_id}/audiences/{audience_id}',
19
- 'DELETE /audience/orgs/{org_id}/audiences/{audience_id}',
20
- 'GET /audience/orgs/{org_id}/audiences/{audience_id}/members',
21
- 'POST /audience/orgs/{org_id}/audiences/{audience_id}/refresh',
22
- ];
23
-
24
- // Canonical filter shape — shared across /people/search, /company/search,
25
- // and embedded in saved audiences. Goldfox-sourced; OR within array, AND
26
- // across fields. Empty filter = all rows in source with default high-only
27
- // confidence.
28
- //
29
- // Some fields are people-source-only (seniority, email_type,
30
- // min_link_confidence) and silently ignored by company-source contexts.
31
- export interface SearchFilter {
32
- // Confidence tier(s) — high (default, 96.4% of dataset, no UGC),
33
- // low (UGC source — spot-check), very_low (UGC + freemail mismatch).
34
- // Pass explicit array to widen beyond the default high-only.
35
- confidence?: ('high' | 'low' | 'very_low')[];
36
-
37
- // ISO 3166-1 alpha-2 country code(s). Consensus across TLD + address + phone.
38
- country?: string[];
39
-
40
- // Only rows where TLD + address + phone all agree on country.
41
- country_consistent?: boolean;
42
-
43
- // Email_type — corporate / freemail / role_based / other_corporate.
44
- // (Ignored on /company/search.)
45
- email_type?: ('corporate' | 'freemail' | 'role_based' | 'other_corporate')[];
46
-
47
- // Behavioral booleans — derived from page crawl signals.
48
- has_c_level?: boolean; // ≥1 person with seniority = c_level
49
- has_careers_page?: boolean; // /careers, /jobs, etc. — growth-stage proxy
50
- has_decision_maker?: boolean; // ≥1 person with seniority ∈ (c_level, vp_director)
51
- has_investors_page?: boolean; // /investors, /ir/ — mature-org proxy
52
- has_shop_page?: boolean; // /shop, /store, /products — e-commerce proxy
53
-
54
- // companies[0].legal_form_country is not null — name has a recognised
55
- // legal suffix (GmbH/Ltd/Inc/SARL/...).
56
- is_registered_entity?: boolean;
57
-
58
- // Case-insensitive substring match on the row's domain.
59
- keyword?: string;
60
-
61
- // Pagination
62
- limit?: number; // default 20, max 100
63
- offset?: number;
64
-
65
- // Row's headcount_lower_bound (≈ ARRAY_LENGTH(people)) must be ≥ this.
66
- min_headcount?: number;
67
-
68
- // Drop people whose link_confidence is below this threshold (0–1).
69
- // 1.0 = email-domain match (definitive); 0.5 = strong; lower = weak.
70
- // (Ignored on /company/search.)
71
- min_link_confidence?: number;
72
-
73
- // Row must appear in at least this many distinct source URLs.
74
- min_source_count?: number;
75
-
76
- // Goldfox seniority — c_level, vp_director, manager, senior_ic, ic.
77
- // (Ignored on /company/search.)
78
- seniority?: ('c_level' | 'vp_director' | 'manager' | 'senior_ic' | 'ic')[];
79
-
80
- // TLD class — cctld / generic / vanity / low_trust / other.
81
- tld_class?: ('cctld' | 'generic' | 'vanity' | 'low_trust' | 'other')[];
82
- }
83
-
84
- export type AudienceSource = 'people' | 'company';
85
-
86
- export interface Audience {
87
- id: string;
88
- org_id: string;
89
- name: string;
90
- description?: string;
91
- source: AudienceSource;
92
- filter: SearchFilter;
93
- member_count: number;
94
- created_at: string;
95
- updated_at: string;
96
- }
97
-
98
- // Backend returns 'people' or 'companies' keyed by source; the response
99
- // itself does NOT carry the source field, so callers either know it from
100
- // the audience object or detect by which array is non-empty.
101
- export interface AudienceMembers {
102
- total: number;
103
- limit: number;
104
- offset: number;
105
- has_more: boolean;
106
- people?: Person[];
107
- companies?: Company[];
108
- }
109
-
110
- export interface AudienceRefreshResult {
111
- total: number; // new member count
112
- previous: number; // count before refresh
113
- delta: number; // total - previous
114
- refreshed_at: string;
115
- }
116
-
117
- export interface CreateAudienceInput {
118
- name: string;
119
- source: AudienceSource;
120
- filter: SearchFilter;
121
- description?: string;
122
- }
123
-
124
- export interface UpdateAudienceInput {
125
- name?: string;
126
- description?: string;
127
- filter?: SearchFilter;
128
- }
129
-
130
- export async function createAudience(apiKey: string, orgId: string, input: CreateAudienceInput): Promise<Audience> {
131
- return request('POST', `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences`, apiKey, input);
132
- }
133
-
134
- export async function listAudiences(apiKey: string, orgId: string): Promise<Audience[]> {
135
- return request('GET', `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences`, apiKey);
136
- }
137
-
138
- export async function getAudience(apiKey: string, orgId: string, audienceId: string): Promise<Audience> {
139
- return request('GET', `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences/${encodeURIComponent(audienceId)}`, apiKey);
140
- }
141
-
142
- export async function updateAudience(apiKey: string, orgId: string, audienceId: string, patch: UpdateAudienceInput): Promise<Audience> {
143
- return request('PATCH', `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences/${encodeURIComponent(audienceId)}`, apiKey, patch);
144
- }
145
-
146
- export async function deleteAudience(apiKey: string, orgId: string, audienceId: string): Promise<void> {
147
- return request('DELETE', `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences/${encodeURIComponent(audienceId)}`, apiKey);
148
- }
149
-
150
- export async function getAudienceMembers(apiKey: string, orgId: string, audienceId: string, opts: { limit?: number; offset?: number } = {}): Promise<AudienceMembers> {
151
- const _q = new URLSearchParams();
152
- for (const [k, v] of Object.entries(opts)) {
153
- if (v != null) _q.append(k, String(v));
154
- }
155
- const _qs = _q.toString();
156
- const _url = _qs ? `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences/${encodeURIComponent(audienceId)}/members?${_qs}` : `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences/${encodeURIComponent(audienceId)}/members`;
157
- return request('GET', _url, apiKey);
158
- }
159
-
160
- export async function refreshAudience(apiKey: string, orgId: string, audienceId: string): Promise<AudienceRefreshResult> {
161
- return request('POST', `${BASE_URL}/audience/orgs/${encodeURIComponent(orgId)}/audiences/${encodeURIComponent(audienceId)}/refresh`, apiKey);
162
- }