@myapihq/sdk 2.4.1 → 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.
package/dist/client.d.ts CHANGED
@@ -5,10 +5,31 @@ export declare class MyApiError extends Error {
5
5
  body?: Record<string, unknown> | undefined;
6
6
  constructor(code: string, status: number, detail?: string | undefined, body?: Record<string, unknown> | undefined);
7
7
  }
8
- export declare function request<T>(method: string, url: string, apiKey?: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<T>;
8
+ /**
9
+ * Identify the calling client in the User-Agent. The CLI calls this at startup
10
+ * with its own version; SDK-only consumers can call it to tag their app. Note
11
+ * that browsers silently drop a caller-set User-Agent — that's expected.
12
+ */
13
+ export declare function setUserAgent(ua: string): void;
14
+ export interface RequestOptions {
15
+ /** Per-attempt deadline in ms. 0 disables. Default 120s, or MYAPI_TIMEOUT_MS. */
16
+ timeoutMs?: number;
17
+ /** Total attempts including the first. Default 3, or MYAPI_MAX_ATTEMPTS. */
18
+ maxAttempts?: number;
19
+ /**
20
+ * Retry non-idempotent methods (POST/PATCH) too. OFF by default: the SDK
21
+ * sends an Idempotency-Key on every mutating call, but until the backend
22
+ * honors it a retried POST is a duplicate charge / duplicate send. Flip this
23
+ * per-call only for endpoints confirmed to deduplicate.
24
+ */
25
+ retryUnsafe?: boolean;
26
+ /** Caller-supplied AbortSignal, composed with the timeout. */
27
+ signal?: AbortSignal;
28
+ }
29
+ export declare function request<T>(method: string, url: string, apiKey?: string, body?: unknown, extraHeaders?: Record<string, string>, opts?: RequestOptions): Promise<T>;
9
30
  export interface Page<T> {
10
31
  data: T[];
11
32
  next_cursor?: string;
12
33
  has_more?: boolean;
13
34
  }
14
- export declare function requestPage<T>(method: string, url: string, apiKey?: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<Page<T>>;
35
+ export declare function requestPage<T>(method: string, url: string, apiKey?: string, body?: unknown, extraHeaders?: Record<string, string>, opts?: RequestOptions): Promise<Page<T>>;
package/dist/client.js CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MyApiError = void 0;
4
+ exports.setUserAgent = setUserAgent;
4
5
  exports.request = request;
5
6
  exports.requestPage = requestPage;
7
+ const version_1 = require("./version");
6
8
  class MyApiError extends Error {
7
9
  code;
8
10
  status;
@@ -23,7 +25,121 @@ class MyApiError extends Error {
23
25
  }
24
26
  }
25
27
  exports.MyApiError = MyApiError;
26
- async function requestFull(method, url, apiKey, body, extraHeaders) {
28
+ // ---------------------------------------------------------------------------
29
+ // Transport policy
30
+ // ---------------------------------------------------------------------------
31
+ // Every call in this SDK is a single `fetch`. Without a deadline that means a
32
+ // hung edge hop hangs the caller forever — and the caller is usually an agent
33
+ // in a loop with no way to notice. These knobs bound that.
34
+ /** Per-attempt deadline. Generous on purpose: bound hangs, don't fight slow
35
+ * but healthy calls (LLM completions, multi-file funnel publishes). */
36
+ const DEFAULT_TIMEOUT_MS = 120_000;
37
+ /** Total attempts, not retries — 3 means at most 2 retries. */
38
+ const DEFAULT_MAX_ATTEMPTS = 3;
39
+ const BACKOFF_BASE_MS = 250;
40
+ const BACKOFF_CAP_MS = 8_000;
41
+ // Transient by definition: the request never reached a handler, or the
42
+ // handler explicitly asked us to come back. 500 is deliberately absent —
43
+ // it's ambiguous (the write may have landed), so retrying it can duplicate.
44
+ const RETRYABLE_STATUS = new Set([408, 429, 502, 503, 504]);
45
+ // Retrying these is safe by HTTP semantics regardless of backend support.
46
+ const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
47
+ // Methods that mutate and therefore get an Idempotency-Key, so the backend
48
+ // can collapse duplicates once it honors the header.
49
+ const KEYED_METHODS = new Set(['POST', 'PATCH', 'PUT']);
50
+ function env(name) {
51
+ // Guarded: `process` is absent in some edge/browser runtimes.
52
+ try {
53
+ return typeof process !== 'undefined' ? process.env?.[name] : undefined;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ function envInt(name, fallback) {
60
+ const raw = env(name);
61
+ if (raw === undefined || raw === '')
62
+ return fallback;
63
+ const n = Number(raw);
64
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
65
+ }
66
+ let configuredUserAgent;
67
+ /**
68
+ * Identify the calling client in the User-Agent. The CLI calls this at startup
69
+ * with its own version; SDK-only consumers can call it to tag their app. Note
70
+ * that browsers silently drop a caller-set User-Agent — that's expected.
71
+ */
72
+ function setUserAgent(ua) {
73
+ configuredUserAgent = ua;
74
+ }
75
+ function userAgent() {
76
+ return configuredUserAgent ?? env('MYAPI_USER_AGENT') ?? `myapihq-sdk/${version_1.SDK_VERSION}`;
77
+ }
78
+ function newIdempotencyKey() {
79
+ const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
80
+ if (c?.randomUUID)
81
+ return c.randomUUID();
82
+ if (c?.getRandomValues) {
83
+ const b = c.getRandomValues(new Uint8Array(16));
84
+ return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
85
+ }
86
+ // Last resort. Only reached on runtimes with no Web Crypto at all; the key
87
+ // still just needs to be unique per logical request, not unguessable.
88
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
89
+ }
90
+ /** Parse Retry-After, which is either delta-seconds or an HTTP-date. */
91
+ function retryAfterMs(header) {
92
+ if (!header)
93
+ return undefined;
94
+ const secs = Number(header);
95
+ if (Number.isFinite(secs))
96
+ return Math.max(0, secs * 1000);
97
+ const when = Date.parse(header);
98
+ if (!Number.isNaN(when))
99
+ return Math.max(0, when - Date.now());
100
+ return undefined;
101
+ }
102
+ /** Exponential backoff with full jitter — spreads a thundering herd of agents
103
+ * retrying the same rate-limited endpoint instead of resynchronising them. */
104
+ function backoffMs(attempt) {
105
+ const ceiling = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * 2 ** (attempt - 1));
106
+ return Math.random() * ceiling;
107
+ }
108
+ function sleep(ms) {
109
+ return new Promise(resolve => setTimeout(resolve, ms));
110
+ }
111
+ function isAbortError(e) {
112
+ return !!e && typeof e === 'object' && e.name === 'AbortError';
113
+ }
114
+ async function fetchOnce(method, url, headers, body, timeoutMs, callerSignal) {
115
+ const options = { method, headers };
116
+ if (body !== undefined)
117
+ options.body = body;
118
+ // No timeout configured and no caller signal — nothing to wire up.
119
+ if (timeoutMs <= 0 && !callerSignal)
120
+ return fetch(url, options);
121
+ const controller = new AbortController();
122
+ const onAbort = () => controller.abort();
123
+ let timer;
124
+ if (timeoutMs > 0)
125
+ timer = setTimeout(() => controller.abort(), timeoutMs);
126
+ if (callerSignal) {
127
+ if (callerSignal.aborted)
128
+ controller.abort();
129
+ else
130
+ callerSignal.addEventListener('abort', onAbort, { once: true });
131
+ }
132
+ options.signal = controller.signal;
133
+ try {
134
+ return await fetch(url, options);
135
+ }
136
+ finally {
137
+ if (timer !== undefined)
138
+ clearTimeout(timer);
139
+ callerSignal?.removeEventListener('abort', onAbort);
140
+ }
141
+ }
142
+ async function requestFull(method, url, apiKey, body, extraHeaders, opts) {
27
143
  const headers = {};
28
144
  if (apiKey) {
29
145
  headers['Authorization'] = `Bearer ${apiKey}`;
@@ -31,6 +147,13 @@ async function requestFull(method, url, apiKey, body, extraHeaders) {
31
147
  if (body !== undefined) {
32
148
  headers['Content-Type'] = 'application/json';
33
149
  }
150
+ headers['User-Agent'] = userAgent();
151
+ const upper = method.toUpperCase();
152
+ // One key for the whole call, reused across attempts — that's the entire
153
+ // point. Generating per-attempt would defeat deduplication.
154
+ if (KEYED_METHODS.has(upper) && !extraHeaders?.['Idempotency-Key']) {
155
+ headers['Idempotency-Key'] = newIdempotencyKey();
156
+ }
34
157
  // Caller-supplied headers (e.g. CAS via If-Match) win over the defaults
35
158
  // above. Keep this near the top so route logic later doesn't accidentally
36
159
  // overwrite something the caller passed in.
@@ -38,14 +161,45 @@ async function requestFull(method, url, apiKey, body, extraHeaders) {
38
161
  for (const [k, v] of Object.entries(extraHeaders))
39
162
  headers[k] = v;
40
163
  }
41
- const options = {
42
- method,
43
- headers,
44
- };
45
- if (body !== undefined) {
46
- options.body = JSON.stringify(body);
164
+ const payload = body !== undefined ? JSON.stringify(body) : undefined;
165
+ const timeoutMs = opts?.timeoutMs ?? envInt('MYAPI_TIMEOUT_MS', DEFAULT_TIMEOUT_MS);
166
+ const maxAttempts = Math.max(1, opts?.maxAttempts ?? envInt('MYAPI_MAX_ATTEMPTS', DEFAULT_MAX_ATTEMPTS));
167
+ const mayRetry = IDEMPOTENT_METHODS.has(upper) || opts?.retryUnsafe === true;
168
+ let attempt = 0;
169
+ // eslint-disable-next-line no-constant-condition
170
+ while (true) {
171
+ attempt++;
172
+ let response;
173
+ try {
174
+ response = await fetchOnce(upper, url, headers, payload, timeoutMs, opts?.signal);
175
+ }
176
+ catch (e) {
177
+ // The caller aborted deliberately — surface that, never retry it.
178
+ if (opts?.signal?.aborted) {
179
+ throw new MyApiError('request_aborted', 0, `${upper} ${url} was aborted by the caller`);
180
+ }
181
+ // Transport never produced a response: DNS failure, connection reset,
182
+ // TLS error, or our own timeout. Previously this escaped as a bare
183
+ // TypeError, which bypassed the CLI's friendlyError entirely.
184
+ const timedOut = isAbortError(e);
185
+ if (mayRetry && attempt < maxAttempts) {
186
+ await sleep(backoffMs(attempt));
187
+ continue;
188
+ }
189
+ if (timedOut) {
190
+ throw new MyApiError('timeout', 0, `${upper} ${url} timed out after ${timeoutMs}ms (attempt ${attempt} of ${maxAttempts})`);
191
+ }
192
+ throw new MyApiError('network_error', 0, `${upper} ${url} failed: ${e?.message ?? String(e)}`);
193
+ }
194
+ if (mayRetry && attempt < maxAttempts && RETRYABLE_STATUS.has(response.status)) {
195
+ const after = retryAfterMs(response.headers.get('retry-after'));
196
+ await sleep(after ?? backoffMs(attempt));
197
+ continue;
198
+ }
199
+ return await parseResponse(upper, url, response);
47
200
  }
48
- const response = await fetch(url, options);
201
+ }
202
+ async function parseResponse(method, url, response) {
49
203
  if (response.status === 204) {
50
204
  return { success: true, data: null, error: null, meta: {} };
51
205
  }
@@ -62,31 +216,29 @@ async function requestFull(method, url, apiKey, body, extraHeaders) {
62
216
  throw new MyApiError('invalid_json_response', response.status, `${method} ${url} returned non-JSON (status ${response.status}): ${snippet}`);
63
217
  }
64
218
  if (!response.ok) {
65
- const err = result?.error;
66
- const code = typeof err === 'object' ? (err?.code || 'unknown_error') : (err || 'unknown_error');
67
- const detail = typeof err === 'object' ? (err?.message || undefined) : undefined;
68
- const errBody = typeof err === 'object' ? err : undefined;
69
- throw new MyApiError(code, response.status, detail, errBody);
219
+ throw toError(result?.error, response.status);
70
220
  }
71
221
  const apiResponse = result;
72
222
  if (!apiResponse.success) {
73
- const err = apiResponse.error;
74
- const code = typeof err === 'object' ? (err?.code || 'unknown_error') : (err || 'unknown_error');
75
- const detail = typeof err === 'object' ? (err?.message || undefined) : undefined;
76
- const errBody = typeof err === 'object' ? err : undefined;
77
- throw new MyApiError(code, response.status, detail, errBody);
223
+ throw toError(apiResponse.error, response.status);
78
224
  }
79
225
  return apiResponse;
80
226
  }
227
+ function toError(err, status) {
228
+ const code = typeof err === 'object' && err !== null ? (err?.code || 'unknown_error') : (err || 'unknown_error');
229
+ const detail = typeof err === 'object' && err !== null ? (err?.message || undefined) : undefined;
230
+ const errBody = typeof err === 'object' && err !== null ? err : undefined;
231
+ return new MyApiError(code, status, detail, errBody);
232
+ }
81
233
  // Returns the unwrapped `data` payload — the common case.
82
- async function request(method, url, apiKey, body, extraHeaders) {
83
- return (await requestFull(method, url, apiKey, body, extraHeaders)).data;
234
+ async function request(method, url, apiKey, body, extraHeaders, opts) {
235
+ return (await requestFull(method, url, apiKey, body, extraHeaders, opts)).data;
84
236
  }
85
237
  // For keyset-paginated endpoints (backend `envelope.WrapPage`): `data` is a
86
238
  // bare array and the cursor/has_more live under `meta`. `request` would drop
87
239
  // the cursor, so list endpoints with pagination must use this.
88
- async function requestPage(method, url, apiKey, body, extraHeaders) {
89
- const r = await requestFull(method, url, apiKey, body, extraHeaders);
240
+ async function requestPage(method, url, apiKey, body, extraHeaders, opts) {
241
+ const r = await requestFull(method, url, apiKey, body, extraHeaders, opts);
90
242
  const meta = (r.meta ?? {});
91
243
  return { data: (r.data ?? []), next_cursor: meta.next_cursor, has_more: meta.has_more };
92
244
  }
package/dist/crm.d.ts CHANGED
@@ -111,6 +111,7 @@ export declare function createContact(apiKey: string, orgId: string, input: Crea
111
111
  export declare function getContact(apiKey: string, orgId: string, id: string): Promise<Contact>;
112
112
  export declare function updateContact(apiKey: string, orgId: string, id: string, patch: UpdateContactInput): Promise<Contact>;
113
113
  export declare function deleteContact(apiKey: string, orgId: string, id: string): Promise<void>;
114
+ export declare function restoreContact(apiKey: string, orgId: string, id: string): Promise<Contact>;
114
115
  export declare function searchContacts(apiKey: string, orgId: string, filter?: ContactSearchFilter): Promise<ContactSearchResult>;
115
116
  export declare function promoteContact(apiKey: string, orgId: string, goldfoxPersonId: string): Promise<Contact>;
116
117
  export declare function getContactEvents(apiKey: string, orgId: string, id: string, opts?: ListEventsOptions): Promise<EventsResponse>;
@@ -118,5 +119,6 @@ export declare function createCompany(apiKey: string, orgId: string, input: Crea
118
119
  export declare function getCompany(apiKey: string, orgId: string, id: string): Promise<Company>;
119
120
  export declare function updateCompany(apiKey: string, orgId: string, id: string, patch: UpdateCompanyInput): Promise<Company>;
120
121
  export declare function deleteCompany(apiKey: string, orgId: string, id: string): Promise<void>;
122
+ export declare function restoreCompany(apiKey: string, orgId: string, id: string): Promise<Company>;
121
123
  export declare function searchCompanies(apiKey: string, orgId: string, filter?: CompanySearchFilter): Promise<CompanySearchResult>;
122
124
  export declare function promoteCompany(apiKey: string, orgId: string, domain: string): Promise<Company>;
package/dist/crm.js CHANGED
@@ -5,6 +5,7 @@ exports.createContact = createContact;
5
5
  exports.getContact = getContact;
6
6
  exports.updateContact = updateContact;
7
7
  exports.deleteContact = deleteContact;
8
+ exports.restoreContact = restoreContact;
8
9
  exports.searchContacts = searchContacts;
9
10
  exports.promoteContact = promoteContact;
10
11
  exports.getContactEvents = getContactEvents;
@@ -12,6 +13,7 @@ exports.createCompany = createCompany;
12
13
  exports.getCompany = getCompany;
13
14
  exports.updateCompany = updateCompany;
14
15
  exports.deleteCompany = deleteCompany;
16
+ exports.restoreCompany = restoreCompany;
15
17
  exports.searchCompanies = searchCompanies;
16
18
  exports.promoteCompany = promoteCompany;
17
19
  const client_1 = require("./client");
@@ -23,6 +25,7 @@ exports.EXPOSES = [
23
25
  'GET /crm/orgs/{org_id}/contacts/{id}',
24
26
  'PATCH /crm/orgs/{org_id}/contacts/{id}',
25
27
  'DELETE /crm/orgs/{org_id}/contacts/{id}',
28
+ 'POST /crm/orgs/{org_id}/contacts/{id}/restore',
26
29
  'GET /crm/orgs/{org_id}/contacts/{id}/events',
27
30
  'POST /crm/orgs/{org_id}/companies',
28
31
  'POST /crm/orgs/{org_id}/companies/promote',
@@ -30,6 +33,7 @@ exports.EXPOSES = [
30
33
  'GET /crm/orgs/{org_id}/companies/{id}',
31
34
  'PATCH /crm/orgs/{org_id}/companies/{id}',
32
35
  'DELETE /crm/orgs/{org_id}/companies/{id}',
36
+ 'POST /crm/orgs/{org_id}/companies/{id}/restore',
33
37
  ];
34
38
  // ── Contacts ──────────────────────────────────────────────────────────────
35
39
  async function createContact(apiKey, orgId, input) {
@@ -44,6 +48,13 @@ async function updateContact(apiKey, orgId, id, patch) {
44
48
  async function deleteContact(apiKey, orgId, id) {
45
49
  return (0, client_1.request)('DELETE', `${config_1.CRM_BASE}/crm/orgs/${encodeURIComponent(orgId)}/contacts/${encodeURIComponent(id)}`, apiKey);
46
50
  }
51
+ // Undo a soft delete. Uses the backend's dedicated restore verb rather than
52
+ // PATCHing `deleted_at: null` — the CLI used to do the latter, which quietly
53
+ // depended on the update handler accepting a field that isn't part of the
54
+ // documented update contract.
55
+ async function restoreContact(apiKey, orgId, id) {
56
+ return (0, client_1.request)('POST', `${config_1.CRM_BASE}/crm/orgs/${encodeURIComponent(orgId)}/contacts/${encodeURIComponent(id)}/restore`, apiKey);
57
+ }
47
58
  async function searchContacts(apiKey, orgId, filter = {}) {
48
59
  return (0, client_1.request)('POST', `${config_1.CRM_BASE}/crm/orgs/${encodeURIComponent(orgId)}/contacts/search`, apiKey, filter);
49
60
  }
@@ -78,6 +89,10 @@ async function updateCompany(apiKey, orgId, id, patch) {
78
89
  async function deleteCompany(apiKey, orgId, id) {
79
90
  return (0, client_1.request)('DELETE', `${config_1.CRM_BASE}/crm/orgs/${encodeURIComponent(orgId)}/companies/${encodeURIComponent(id)}`, apiKey);
80
91
  }
92
+ // See restoreContact — same contract, companies half.
93
+ async function restoreCompany(apiKey, orgId, id) {
94
+ return (0, client_1.request)('POST', `${config_1.CRM_BASE}/crm/orgs/${encodeURIComponent(orgId)}/companies/${encodeURIComponent(id)}/restore`, apiKey);
95
+ }
81
96
  async function searchCompanies(apiKey, orgId, filter = {}) {
82
97
  return (0, client_1.request)('POST', `${config_1.CRM_BASE}/crm/orgs/${encodeURIComponent(orgId)}/companies/search`, apiKey, filter);
83
98
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from './types';
2
2
  export * from './client';
3
3
  export * from './funds';
4
+ export * from './version';
4
5
  export * as hq from './hq';
5
6
  export * as auth from './auth';
6
7
  export * as domain from './domain';
package/dist/index.js CHANGED
@@ -40,6 +40,7 @@ exports.task = exports.queue = exports.git = exports.container = exports.payment
40
40
  __exportStar(require("./types"), exports);
41
41
  __exportStar(require("./client"), exports);
42
42
  __exportStar(require("./funds"), exports);
43
+ __exportStar(require("./version"), exports);
43
44
  // Note: config constants (STORAGE_BASE etc.) are NOT re-exported from the
44
45
  // barrel. TypeScript compiles `export * from './config'` to a runtime
45
46
  // `__exportStar` call that Node's cjs-module-lexer can't see through, so
@@ -0,0 +1 @@
1
+ export declare const SDK_VERSION = "2.4.2";
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SDK_VERSION = void 0;
4
+ // Kept in lockstep with package.json by scripts/release.mjs, and guarded by
5
+ // packages/cli/src/sdk-version.test.ts so a hand-edited package.json can't
6
+ // silently strand it.
7
+ //
8
+ // Why a constant and not a package.json read: the SDK runs inside edge
9
+ // functions (Cloudflare Workers), so it must not import node:fs. A literal
10
+ // is the only version source that works in every runtime we ship to.
11
+ exports.SDK_VERSION = '2.4.2';
package/package.json CHANGED
@@ -1,13 +1,22 @@
1
1
  {
2
2
  "name": "@myapihq/sdk",
3
3
  "license": "Apache-2.0",
4
- "version": "2.4.1",
4
+ "version": "2.4.2",
5
5
  "description": "TypeScript SDK for the MyAPI ecosystem",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/myapihq/myapi.git",
9
+ "directory": "packages/sdk"
10
+ },
6
11
  "main": "dist/index.js",
7
12
  "types": "dist/index.d.ts",
13
+ "files": [
14
+ "dist"
15
+ ],
8
16
  "scripts": {
9
17
  "build": "tsc",
10
- "dev": "tsc --watch"
18
+ "dev": "tsc --watch",
19
+ "test": "cd ../cli && npx vitest run src"
11
20
  },
12
21
  "devDependencies": {
13
22
  "@types/node": "^25.6.0",