@automate.ax/integration-contracts 0.145.2 → 0.146.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.
@@ -0,0 +1,43 @@
1
+ import { type Encodable } from "@automate.ax/codec";
2
+ import type { HighLevelOperationKey } from "./operation-manifest.js";
3
+ import type { HighLevelOperationInput, HighLevelOperationOutput } from "./types.js";
4
+ /** Resolved account accepted by the shared HighLevel API client. */
5
+ export interface HighLevelResolvedAccount {
6
+ connectionMethodId: string;
7
+ secret: Record<string, unknown>;
8
+ serviceId: "highlevel";
9
+ }
10
+ /** Structured error returned by a rejected HighLevel request. */
11
+ export declare class HighLevelApiError extends Error {
12
+ readonly body?: Encodable;
13
+ readonly retryAfter?: string;
14
+ readonly status: number;
15
+ /**
16
+ * Creates an error from one rejected HighLevel response.
17
+ *
18
+ * @param options - Rejected response details.
19
+ * @param options.body - Encodable provider error body.
20
+ * @param options.retryAfter - Provider retry timing.
21
+ * @param options.status - HTTP status.
22
+ */
23
+ constructor(options: {
24
+ body?: Encodable;
25
+ retryAfter?: string;
26
+ status: number;
27
+ });
28
+ }
29
+ /**
30
+ * Creates an authenticated client for the frozen HighLevel action surface.
31
+ *
32
+ * @param account - Resolved HighLevel account.
33
+ * @throws When the connection method or secret is invalid.
34
+ */
35
+ export declare function getHighLevelApi(account: HighLevelResolvedAccount): {
36
+ /**
37
+ * Executes and validates one named HighLevel OpenAPI operation.
38
+ *
39
+ * @param key - Frozen operation key.
40
+ * @param input - Flattened operation input.
41
+ */
42
+ operation<TKey extends HighLevelOperationKey>(key: TKey, input: HighLevelOperationInput<TKey>): Promise<HighLevelOperationOutput<TKey>>;
43
+ };
@@ -0,0 +1,236 @@
1
+ import { encodableSchema } from "@automate.ax/codec";
2
+ import * as z from "zod";
3
+ import { highLevelOperation, highLevelOperationOutputSchema } from "./schemas.js";
4
+ const HIGH_LEVEL_API_BASE_URL = "https://services.leadconnectorhq.com/";
5
+ const HIGH_LEVEL_API_ORIGIN = new URL(HIGH_LEVEL_API_BASE_URL).origin;
6
+ const HIGH_LEVEL_SECRET_SCHEMA = z.object({
7
+ accessToken: z.string().min(1),
8
+ locationId: z.string().min(1),
9
+ });
10
+ /** Structured error returned by a rejected HighLevel request. */
11
+ export class HighLevelApiError extends Error {
12
+ body;
13
+ retryAfter;
14
+ status;
15
+ /**
16
+ * Creates an error from one rejected HighLevel response.
17
+ *
18
+ * @param options - Rejected response details.
19
+ * @param options.body - Encodable provider error body.
20
+ * @param options.retryAfter - Provider retry timing.
21
+ * @param options.status - HTTP status.
22
+ */
23
+ constructor(options) {
24
+ super(getErrorMessage(options.body) ??
25
+ `HighLevel API request failed with status ${options.status}.`);
26
+ this.name = "HighLevelApiError";
27
+ this.body = options.body;
28
+ this.retryAfter = options.retryAfter;
29
+ this.status = options.status;
30
+ }
31
+ }
32
+ /**
33
+ * Creates an authenticated client for the frozen HighLevel action surface.
34
+ *
35
+ * @param account - Resolved HighLevel account.
36
+ * @throws When the connection method or secret is invalid.
37
+ */
38
+ export function getHighLevelApi(account) {
39
+ const credentials = HIGH_LEVEL_SECRET_SCHEMA.parse(account.secret);
40
+ if (account.connectionMethodId !== "oauth" &&
41
+ account.connectionMethodId !== "private-integration-token") {
42
+ throw new Error(`Unsupported HighLevel connection method: ${account.connectionMethodId}`);
43
+ }
44
+ return {
45
+ /**
46
+ * Executes and validates one named HighLevel OpenAPI operation.
47
+ *
48
+ * @param key - Frozen operation key.
49
+ * @param input - Flattened operation input.
50
+ */
51
+ async operation(key, input) {
52
+ const definition = highLevelOperation(key);
53
+ const pathValues = new Map();
54
+ const queryValues = new Map();
55
+ const bodyValues = {};
56
+ const inputRecord = input;
57
+ for (const name of definition.pathParameters) {
58
+ pathValues.set(name, inputRecord[name]);
59
+ }
60
+ for (const { name } of definition.queryParameters) {
61
+ queryValues.set(name, inputRecord[name]);
62
+ }
63
+ for (const name of definition.bodyParameters) {
64
+ if (inputRecord[name] !== undefined)
65
+ bodyValues[name] = inputRecord[name];
66
+ }
67
+ for (const placement of definition.locationPlacements) {
68
+ if (placement === "path")
69
+ pathValues.set("locationId", credentials.locationId);
70
+ if (placement === "query")
71
+ queryValues.set("locationId", credentials.locationId);
72
+ if (placement === "body")
73
+ bodyValues.locationId = credentials.locationId;
74
+ }
75
+ // Resolve path placeholders before applying the fixed-origin guard.
76
+ const path = definition.path.replace(/\{([^}]+)\}/g, (_match, name) => encodePathSegment(pathValues.get(name), name));
77
+ const normalizedPath = path.replace(/^\/+/, "");
78
+ const url = new URL(normalizedPath, HIGH_LEVEL_API_BASE_URL);
79
+ if (!normalizedPath ||
80
+ normalizedPath.includes("://") ||
81
+ normalizedPath.includes("\\") ||
82
+ normalizedPath.includes("?") ||
83
+ normalizedPath.includes("#") ||
84
+ url.origin !== HIGH_LEVEL_API_ORIGIN) {
85
+ throw new TypeError("HighLevel API paths must remain provider-relative.");
86
+ }
87
+ for (const parameter of definition.queryParameters) {
88
+ appendQuery(url, { ...parameter, name: parameter.providerName }, queryValues.get(parameter.name));
89
+ }
90
+ if (definition.locationPlacements.includes("query")) {
91
+ url.searchParams.set(definition.locationParameterNames.query ?? "locationId", credentials.locationId);
92
+ }
93
+ const hasBody = definition.bodyParameters.length > 0 ||
94
+ definition.locationPlacements.includes("body");
95
+ const response = await fetch(url, {
96
+ body: hasBody ? JSON.stringify(bodyValues) : undefined,
97
+ headers: {
98
+ Accept: "application/json",
99
+ Authorization: `Bearer ${credentials.accessToken}`,
100
+ Version: definition.apiVersion,
101
+ ...(hasBody ? { "Content-Type": "application/json" } : {}),
102
+ },
103
+ method: definition.method,
104
+ redirect: "error",
105
+ });
106
+ const parsed = response.ok && definition.responseMode === "binary"
107
+ ? {
108
+ contentType: response.headers.get("Content-Type") ??
109
+ "application/octet-stream",
110
+ data: Buffer.from(await response.arrayBuffer()).toString("base64"),
111
+ }
112
+ : await parseResponseText(response);
113
+ if (!response.ok) {
114
+ const body = encodableSchema.safeParse(parsed);
115
+ throw new HighLevelApiError({
116
+ ...(body.success && { body: body.data }),
117
+ retryAfter: getRetryAfter(response.headers),
118
+ status: response.status,
119
+ });
120
+ }
121
+ return highLevelOperationOutputSchema(key).parse(parsed);
122
+ },
123
+ };
124
+ }
125
+ /**
126
+ * Encodes one required path parameter.
127
+ *
128
+ * @param value - Parameter value.
129
+ * @param name - Parameter name.
130
+ * @throws When the value is not scalar.
131
+ */
132
+ function encodePathSegment(value, name) {
133
+ if (typeof value !== "number" && typeof value !== "string") {
134
+ throw new TypeError(`HighLevel path parameter ${name} is required.`);
135
+ }
136
+ return encodeURIComponent(String(value));
137
+ }
138
+ /**
139
+ * Appends one OpenAPI query parameter.
140
+ *
141
+ * @param url - Provider request URL.
142
+ * @param parameter - Query serialization metadata.
143
+ * @param parameter.explode - Whether array items repeat the query key.
144
+ * @param parameter.name - Query parameter name.
145
+ * @param parameter.style - OpenAPI serialization style.
146
+ * @param value - Parameter value.
147
+ * @throws When the value is not scalar or an array of scalars.
148
+ */
149
+ function appendQuery(url, parameter, value) {
150
+ if (value == null)
151
+ return;
152
+ if (Array.isArray(value)) {
153
+ if (parameter.style === "form" && parameter.explode) {
154
+ for (const item of value)
155
+ url.searchParams.append(parameter.name, stringifyQueryValue(item));
156
+ }
157
+ else {
158
+ url.searchParams.set(parameter.name, value.map(stringifyQueryValue).join(","));
159
+ }
160
+ return;
161
+ }
162
+ url.searchParams.set(parameter.name, stringifyQueryValue(value));
163
+ }
164
+ /**
165
+ * Converts one supported query value to text.
166
+ *
167
+ * @param value - Query value.
168
+ * @throws When the value is not scalar.
169
+ */
170
+ function stringifyQueryValue(value) {
171
+ if (typeof value !== "boolean" &&
172
+ typeof value !== "number" &&
173
+ typeof value !== "string") {
174
+ throw new TypeError("HighLevel query parameters must be scalar.");
175
+ }
176
+ return String(value);
177
+ }
178
+ /**
179
+ * Reads provider-directed retry timing.
180
+ *
181
+ * @param headers - Rejected response headers.
182
+ */
183
+ function getRetryAfter(headers) {
184
+ const retryAfter = headers.get("Retry-After");
185
+ if (retryAfter)
186
+ return retryAfter;
187
+ const interval = headers.get("X-RateLimit-Interval-Milliseconds");
188
+ if (!interval || !/^\d+$/.test(interval))
189
+ return undefined;
190
+ return String(Math.ceil(Number(interval) / 1_000));
191
+ }
192
+ /**
193
+ * Extracts a provider error message.
194
+ *
195
+ * @param value - Encodable provider error body.
196
+ */
197
+ function getErrorMessage(value) {
198
+ if (!isPlainObject(value))
199
+ return undefined;
200
+ for (const key of ["message", "error", "error_description"]) {
201
+ const candidate = value[key];
202
+ if (typeof candidate === "string" && candidate.trim())
203
+ return candidate;
204
+ }
205
+ return undefined;
206
+ }
207
+ /**
208
+ * Narrows a value to a plain object.
209
+ *
210
+ * @param value - Candidate value.
211
+ */
212
+ function isPlainObject(value) {
213
+ return typeof value === "object" && value !== null && !Array.isArray(value);
214
+ }
215
+ /**
216
+ * Parses JSON while retaining non-JSON provider bodies.
217
+ *
218
+ * @param value - Response body text.
219
+ */
220
+ function parseJson(value) {
221
+ try {
222
+ return JSON.parse(value);
223
+ }
224
+ catch {
225
+ return { response: value };
226
+ }
227
+ }
228
+ /**
229
+ * Reads one text or JSON provider response.
230
+ *
231
+ * @param response - Provider response to decode.
232
+ */
233
+ async function parseResponseText(response) {
234
+ const text = await response.text();
235
+ return text ? parseJson(text) : null;
236
+ }