@automate.ax/integration-contracts 0.93.1 → 0.95.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.
@@ -241,7 +241,7 @@ const CLOSE_TASK_FIELDS = {
241
241
  dueDate: NULLABLE_DATE_SCHEMA.optional(),
242
242
  id: z.string(),
243
243
  isComplete: z.boolean(),
244
- isDateless: z.boolean().optional(),
244
+ isDateless: z.boolean().nullable().optional(),
245
245
  leadId: z.string().nullable(),
246
246
  leadName: z.string().nullable().optional(),
247
247
  objectId: z.string().nullable().optional(),
@@ -44,6 +44,7 @@ interface JobNimbusRequestOptions<TSchema extends z.ZodType> {
44
44
  /** Error returned by a rejected JobNimbus API request. */
45
45
  export class JobNimbusApiError extends Error {
46
46
  readonly details: Encodable
47
+ readonly retryAfter?: string
47
48
  readonly status: number
48
49
 
49
50
  /**
@@ -51,11 +52,13 @@ export class JobNimbusApiError extends Error {
51
52
  *
52
53
  * @param status - HTTP response status.
53
54
  * @param details - Codec-safe response details.
55
+ * @param retryAfter - Raw provider retry timing, when present.
54
56
  */
55
- constructor(status: number, details: Encodable) {
57
+ constructor(status: number, details: Encodable, retryAfter?: string) {
56
58
  super(`JobNimbus API request failed (${status}).`)
57
59
  this.name = "JobNimbusApiError"
58
60
  this.details = details
61
+ this.retryAfter = retryAfter
59
62
  this.status = status
60
63
  }
61
64
  }
@@ -139,6 +142,7 @@ export function getJobNimbusApi(secret: unknown, api: JobNimbusApi) {
139
142
  throw new JobNimbusApiError(
140
143
  response.status,
141
144
  parsed.success ? parsed.data : { response: text },
145
+ response.headers.get("Retry-After") ?? undefined,
142
146
  )
143
147
  }
144
148
  return options.responseSchema.parse(parsed.data)
@@ -162,9 +166,11 @@ export async function downloadJobNimbusFile(secret: unknown, fileId: string) {
162
166
  { headers: { Authorization: `Bearer ${apiKey}` } },
163
167
  )
164
168
  if (!response.ok) {
165
- throw new JobNimbusApiError(response.status, {
166
- response: await response.text(),
167
- })
169
+ throw new JobNimbusApiError(
170
+ response.status,
171
+ { response: await response.text() },
172
+ response.headers.get("Retry-After") ?? undefined,
173
+ )
168
174
  }
169
175
  return response.blob()
170
176
  }
package/src/trello/api.ts CHANGED
@@ -7,6 +7,7 @@ const TRELLO_SECRET_SCHEMA = z.object({
7
7
  token: z.string().min(1),
8
8
  })
9
9
  const TRELLO_ERROR_SCHEMA = z.looseObject({
10
+ error: z.string().optional(),
10
11
  message: z.string().optional(),
11
12
  })
12
13
 
@@ -15,7 +16,7 @@ type TrelloQueryValue = boolean | number | string | undefined
15
16
 
16
17
  /** Options for one authenticated Trello REST request. */
17
18
  export interface TrelloRequestOptions<TSchema extends z.ZodType> {
18
- /** JSON request body for endpoints that accept one. */
19
+ /** JSON or multipart request body for endpoints that accept one. */
19
20
  body?: unknown
20
21
 
21
22
  /** HTTP verb. Defaults to `GET`. */
@@ -44,9 +45,15 @@ export interface TrelloApi {
44
45
 
45
46
  /** Structured Trello REST request error. */
46
47
  export class TrelloApiError extends Error {
48
+ /** Provider error code, or an HTTP fallback code. */
49
+ readonly code: string
50
+
47
51
  /** Provider error body, when Trello returned text. */
48
52
  readonly providerMessage?: string
49
53
 
54
+ /** Rate-limit budget returned with the failed request. */
55
+ readonly rateLimit?: TrelloRateLimitMetadata
56
+
50
57
  /** Retry delay from Trello's `Retry-After` header, in seconds. */
51
58
  readonly retryAfter?: number
52
59
 
@@ -57,12 +64,16 @@ export class TrelloApiError extends Error {
57
64
  * Creates a structured Trello API error.
58
65
  *
59
66
  * @param options - Provider and transport error details.
67
+ * @param options.code - Provider error code or HTTP fallback.
60
68
  * @param options.providerMessage - Provider error body.
69
+ * @param options.rateLimit - Provider rate-limit budget.
61
70
  * @param options.retryAfter - Retry delay in seconds.
62
71
  * @param options.status - HTTP response status.
63
72
  */
64
73
  constructor(options: {
74
+ code: string
65
75
  providerMessage?: string
76
+ rateLimit?: TrelloRateLimitMetadata
66
77
  retryAfter?: number
67
78
  status: number
68
79
  }) {
@@ -72,12 +83,35 @@ export class TrelloApiError extends Error {
72
83
  : `Trello API request failed with status ${options.status}.`,
73
84
  )
74
85
  this.name = "TrelloApiError"
86
+ this.code = options.code
75
87
  this.providerMessage = options.providerMessage
88
+ this.rateLimit = options.rateLimit
76
89
  this.retryAfter = options.retryAfter
77
90
  this.status = options.status
78
91
  }
79
92
  }
80
93
 
94
+ /** One Trello key or token request budget. */
95
+ export interface TrelloRateLimitBudget {
96
+ /** Length of the provider budget window in milliseconds. */
97
+ intervalMs: number
98
+
99
+ /** Maximum requests in the provider budget window. */
100
+ max: number
101
+
102
+ /** Requests remaining in the current provider budget window. */
103
+ remaining: number
104
+ }
105
+
106
+ /** Rate-limit budgets returned by Trello response headers. */
107
+ export interface TrelloRateLimitMetadata {
108
+ /** Shared API-key request budget. */
109
+ apiKey?: TrelloRateLimitBudget
110
+
111
+ /** Connected user-token request budget. */
112
+ apiToken?: TrelloRateLimitBudget
113
+ }
114
+
81
115
  /**
82
116
  * Creates a raw authenticated Trello REST helper.
83
117
  *
@@ -102,12 +136,17 @@ export function getTrelloApi(secret: Record<string, unknown>): TrelloApi {
102
136
  url.searchParams.set("key", apiKey)
103
137
  url.searchParams.set("token", token)
104
138
 
139
+ const isMultipart = options.body instanceof FormData
105
140
  const headers = new Headers({ Accept: "application/json" })
106
- if (options.body !== undefined)
141
+ if (options.body !== undefined && !isMultipart)
107
142
  headers.set("Content-Type", "application/json")
108
143
  const response = await fetch(url, {
109
144
  body:
110
- options.body === undefined ? undefined : JSON.stringify(options.body),
145
+ options.body === undefined
146
+ ? undefined
147
+ : options.body instanceof FormData
148
+ ? options.body
149
+ : JSON.stringify(options.body),
111
150
  headers,
112
151
  method: options.method ?? "GET",
113
152
  })
@@ -115,10 +154,15 @@ export function getTrelloApi(secret: Record<string, unknown>): TrelloApi {
115
154
  const responseText = await response.text()
116
155
  const error = TRELLO_ERROR_SCHEMA.safeParse(parseJson(responseText))
117
156
  throw new TrelloApiError({
157
+ code:
158
+ error.success && error.data.error
159
+ ? error.data.error
160
+ : `http_${response.status}`,
118
161
  providerMessage:
119
162
  error.success && error.data.message
120
163
  ? error.data.message
121
164
  : responseText || undefined,
165
+ rateLimit: parseRateLimitMetadata(response.headers),
122
166
  retryAfter: parseRetryAfter(response.headers.get("Retry-After")),
123
167
  status: response.status,
124
168
  })
@@ -128,6 +172,40 @@ export function getTrelloApi(secret: Record<string, unknown>): TrelloApi {
128
172
  }
129
173
  }
130
174
 
175
+ /**
176
+ * Parses Trello's key and token request budgets from response headers.
177
+ *
178
+ * @param headers - Provider response headers.
179
+ */
180
+ function parseRateLimitMetadata(headers: Headers) {
181
+ const apiKey = parseRateLimitBudget(headers, "api-key")
182
+ const apiToken = parseRateLimitBudget(headers, "api-token")
183
+ if (!apiKey && !apiToken) return undefined
184
+ return { apiKey, apiToken } satisfies TrelloRateLimitMetadata
185
+ }
186
+
187
+ /**
188
+ * Parses one complete Trello request budget.
189
+ *
190
+ * @param headers - Provider response headers.
191
+ * @param budget - Provider budget header prefix.
192
+ */
193
+ function parseRateLimitBudget(
194
+ headers: Headers,
195
+ budget: "api-key" | "api-token",
196
+ ) {
197
+ const intervalMs = parseFiniteNumber(
198
+ headers.get(`x-rate-limit-${budget}-interval-ms`),
199
+ )
200
+ const max = parseFiniteNumber(headers.get(`x-rate-limit-${budget}-max`))
201
+ const remaining = parseFiniteNumber(
202
+ headers.get(`x-rate-limit-${budget}-remaining`),
203
+ )
204
+ if (intervalMs === undefined || max === undefined || remaining === undefined)
205
+ return undefined
206
+ return { intervalMs, max, remaining } satisfies TrelloRateLimitBudget
207
+ }
208
+
131
209
  /**
132
210
  * Accepts a raw card ID, short link, or standard Trello card URL.
133
211
  *
@@ -184,7 +262,16 @@ function parseJson(value: string): unknown {
184
262
  * @param value - Raw `Retry-After` header.
185
263
  */
186
264
  function parseRetryAfter(value: string | null) {
265
+ return parseFiniteNumber(value)
266
+ }
267
+
268
+ /**
269
+ * Parses one finite numeric response header.
270
+ *
271
+ * @param value - Raw response header value.
272
+ */
273
+ function parseFiniteNumber(value: string | null) {
187
274
  if (value === null) return undefined
188
- const seconds = Number(value)
189
- return Number.isFinite(seconds) ? seconds : undefined
275
+ const number = Number(value)
276
+ return Number.isFinite(number) ? number : undefined
190
277
  }