@automate.ax/integration-contracts 0.89.2 → 0.91.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.
@@ -330,11 +330,11 @@ export declare const whatsappTriggerContracts: {
330
330
  video: "video";
331
331
  location: "location";
332
332
  system: "system";
333
+ order: "order";
333
334
  reaction: "reaction";
334
335
  button: "button";
335
336
  contacts: "contacts";
336
337
  interactive: "interactive";
337
- order: "order";
338
338
  sticker: "sticker";
339
339
  unsupported: "unsupported";
340
340
  }>;
@@ -425,11 +425,11 @@ export declare const whatsappTriggerContracts: {
425
425
  video: "video";
426
426
  location: "location";
427
427
  system: "system";
428
+ order: "order";
428
429
  reaction: "reaction";
429
430
  button: "button";
430
431
  contacts: "contacts";
431
432
  interactive: "interactive";
432
- order: "order";
433
433
  sticker: "sticker";
434
434
  unsupported: "unsupported";
435
435
  }>;
@@ -96,11 +96,11 @@ export declare const WHATSAPP_WEBHOOK_SCHEMA: z.ZodObject<{
96
96
  video: "video";
97
97
  location: "location";
98
98
  system: "system";
99
+ order: "order";
99
100
  reaction: "reaction";
100
101
  button: "button";
101
102
  contacts: "contacts";
102
103
  interactive: "interactive";
103
- order: "order";
104
104
  sticker: "sticker";
105
105
  unsupported: "unsupported";
106
106
  }>;
@@ -189,11 +189,11 @@ export declare const WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA: z.ZodObject<{
189
189
  video: "video";
190
190
  location: "location";
191
191
  system: "system";
192
+ order: "order";
192
193
  reaction: "reaction";
193
194
  button: "button";
194
195
  contacts: "contacts";
195
196
  interactive: "interactive";
196
- order: "order";
197
197
  sticker: "sticker";
198
198
  unsupported: "unsupported";
199
199
  }>;
@@ -284,11 +284,11 @@ export declare const WHATSAPP_MESSAGE_RECEIVED_EVENT_SCHEMA: z.ZodObject<{
284
284
  video: "video";
285
285
  location: "location";
286
286
  system: "system";
287
+ order: "order";
287
288
  reaction: "reaction";
288
289
  button: "button";
289
290
  contacts: "contacts";
290
291
  interactive: "interactive";
291
- order: "order";
292
292
  sticker: "sticker";
293
293
  unsupported: "unsupported";
294
294
  }>;
@@ -768,7 +768,7 @@ export declare function normalizeWhatsAppWebhook(input: unknown): {
768
768
  message: {
769
769
  id: string;
770
770
  timestamp: string;
771
- type: "unknown" | "text" | "document" | "audio" | "image" | "video" | "location" | "system" | "reaction" | "button" | "contacts" | "interactive" | "order" | "sticker" | "unsupported";
771
+ type: "unknown" | "text" | "document" | "audio" | "image" | "video" | "location" | "system" | "order" | "reaction" | "button" | "contacts" | "interactive" | "sticker" | "unsupported";
772
772
  content?: z.core.util.JSONType | undefined;
773
773
  context?: {
774
774
  messageId: string;
@@ -781,7 +781,7 @@ export declare function normalizeWhatsAppWebhook(input: unknown): {
781
781
  from: string;
782
782
  id: string;
783
783
  timestamp: string;
784
- type: "unknown" | "text" | "document" | "audio" | "image" | "video" | "location" | "system" | "reaction" | "button" | "contacts" | "interactive" | "order" | "sticker" | "unsupported";
784
+ type: "unknown" | "text" | "document" | "audio" | "image" | "video" | "location" | "system" | "order" | "reaction" | "button" | "contacts" | "interactive" | "sticker" | "unsupported";
785
785
  audio?: {
786
786
  id: string;
787
787
  mime_type?: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/integration-contracts",
3
- "version": "0.89.2",
3
+ "version": "0.91.0",
4
4
  "description": "Shared integration payload contracts and provider primitives for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "./google-forms": "./src/google-forms/index.ts",
29
29
  "./google-meet": "./src/google-meet/index.ts",
30
30
  "./github": "./src/github/index.ts",
31
+ "./jobnimbus": "./src/jobnimbus/index.ts",
31
32
  "./linear": "./src/linear/index.ts",
32
33
  "./notion": "./src/notion/index.ts",
33
34
  "./outlook": "./src/outlook/index.ts",
@@ -47,7 +48,7 @@
47
48
  }
48
49
  },
49
50
  "dependencies": {
50
- "@automate.ax/codec": "0.89.2",
51
+ "@automate.ax/codec": "0.91.0",
51
52
  "@cfworker/json-schema": "^4.1.1",
52
53
  "@googleapis/calendar": "^15.0.0",
53
54
  "@googleapis/forms": "^6.0.1",
@@ -148,6 +149,11 @@
148
149
  "types": "./dist/github/index.d.ts",
149
150
  "default": "./dist/github/index.js"
150
151
  },
152
+ "./jobnimbus": {
153
+ "bun": "./src/jobnimbus/index.ts",
154
+ "types": "./dist/jobnimbus/index.d.ts",
155
+ "default": "./dist/jobnimbus/index.js"
156
+ },
151
157
  "./linear": {
152
158
  "bun": "./src/linear/index.ts",
153
159
  "types": "./dist/linear/index.d.ts",
@@ -0,0 +1,294 @@
1
+ import { isEncodable, type Encodable } from "@automate.ax/codec"
2
+ import * as z from "zod"
3
+
4
+ const JOB_NIMBUS_API_ORIGINS = {
5
+ accountsReceivable: "https://api.jobnimbus.com/accounts-receivable/v1/",
6
+ activities: "https://api.jobnimbus.com/activities/v1/",
7
+ files: "https://api.jobnimbus.com/files/v1/",
8
+ identity: "https://api.jobnimbus.com/identity/v1/",
9
+ public: "https://app.jobnimbus.com/api1/",
10
+ } as const
11
+
12
+ const JOB_NIMBUS_SECRET_SCHEMA = z.object({
13
+ apiKey: z.string().trim().min(1),
14
+ })
15
+
16
+ export const JOB_NIMBUS_VALUE_SCHEMA = z.custom<Encodable>(isEncodable, {
17
+ message: "Expected a codec-safe JobNimbus value.",
18
+ })
19
+
20
+ export const JOB_NIMBUS_RESPONSE_SCHEMA = z.union([
21
+ JOB_NIMBUS_VALUE_SCHEMA,
22
+ z.undefined().transform(() => null),
23
+ ])
24
+
25
+ export type JobNimbusApi = keyof typeof JOB_NIMBUS_API_ORIGINS
26
+
27
+ interface JobNimbusRequestOptions<TSchema extends z.ZodType> {
28
+ body?: Encodable
29
+ contentType?: string
30
+ headers?: Record<string, string | undefined>
31
+ method?: "DELETE" | "GET" | "PATCH" | "POST" | "PUT"
32
+ query?: Record<
33
+ string,
34
+ | boolean
35
+ | number
36
+ | string
37
+ | readonly (boolean | number | string)[]
38
+ | null
39
+ | undefined
40
+ >
41
+ responseSchema: TSchema
42
+ }
43
+
44
+ /** Error returned by a rejected JobNimbus API request. */
45
+ export class JobNimbusApiError extends Error {
46
+ readonly details: Encodable
47
+ readonly status: number
48
+
49
+ /**
50
+ * Creates an error from a rejected JobNimbus response.
51
+ *
52
+ * @param status - HTTP response status.
53
+ * @param details - Codec-safe response details.
54
+ */
55
+ constructor(status: number, details: Encodable) {
56
+ super(`JobNimbus API request failed (${status}).`)
57
+ this.name = "JobNimbusApiError"
58
+ this.details = details
59
+ this.status = status
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Creates an authenticated client for one documented JobNimbus API root.
65
+ *
66
+ * @param secret - Stored JobNimbus API key.
67
+ * @param api - Public API or one current Platform service.
68
+ */
69
+ export function getJobNimbusApi(secret: unknown, api: JobNimbusApi) {
70
+ const { apiKey } = JOB_NIMBUS_SECRET_SCHEMA.parse(secret)
71
+ const origin = JOB_NIMBUS_API_ORIGINS[api]
72
+ const root = new URL(origin)
73
+
74
+ return {
75
+ /**
76
+ * Sends one JSON request below the selected JobNimbus API root.
77
+ *
78
+ * @param path - Relative path below the selected root.
79
+ * @param options - Request method, query, body, headers, and response
80
+ * schema.
81
+ */
82
+ async request<TSchema extends z.ZodType>(
83
+ path: string,
84
+ options: JobNimbusRequestOptions<TSchema>,
85
+ ): Promise<z.output<TSchema>> {
86
+ const normalizedPath = path.replace(/^\/+/, "")
87
+ if (
88
+ !normalizedPath ||
89
+ normalizedPath.includes("://") ||
90
+ normalizedPath.includes("\\")
91
+ ) {
92
+ throw new TypeError("JobNimbus API paths must be relative.")
93
+ }
94
+
95
+ const url = new URL(normalizedPath, origin)
96
+ if (
97
+ url.origin !== root.origin ||
98
+ !url.pathname.startsWith(root.pathname)
99
+ ) {
100
+ throw new TypeError(
101
+ "JobNimbus API paths must remain below the selected root.",
102
+ )
103
+ }
104
+ for (const [key, value] of Object.entries(options.query ?? {})) {
105
+ if (value == null) continue
106
+ if (Array.isArray(value)) {
107
+ for (const item of value) url.searchParams.append(key, String(item))
108
+ } else {
109
+ url.searchParams.set(key, String(value))
110
+ }
111
+ }
112
+
113
+ const headers = new Headers({
114
+ Accept: "application/json",
115
+ Authorization: `Bearer ${apiKey}`,
116
+ })
117
+ for (const [key, value] of Object.entries(options.headers ?? {})) {
118
+ if (value !== undefined) headers.set(key, value)
119
+ }
120
+ if (options.body !== undefined) {
121
+ headers.set("Content-Type", options.contentType ?? "application/json")
122
+ }
123
+
124
+ const response = await fetch(url, {
125
+ body:
126
+ options.body === undefined
127
+ ? undefined
128
+ : JSON.stringify(
129
+ api === "public" ? toJobNimbus(options.body) : options.body,
130
+ ),
131
+ headers,
132
+ method: options.method ?? "GET",
133
+ })
134
+ const text = await response.text()
135
+ const parsed = JOB_NIMBUS_RESPONSE_SCHEMA.safeParse(
136
+ fromJobNimbus(text ? parseJson(text) : null),
137
+ )
138
+ if (!response.ok || !parsed.success) {
139
+ throw new JobNimbusApiError(
140
+ response.status,
141
+ parsed.success ? parsed.data : { response: text },
142
+ )
143
+ }
144
+ return options.responseSchema.parse(parsed.data)
145
+ },
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Downloads one JobNimbus attachment from its documented non-API route.
151
+ *
152
+ * @param secret - Stored JobNimbus API key.
153
+ * @param fileId - Attachment JNID.
154
+ */
155
+ export async function downloadJobNimbusFile(secret: unknown, fileId: string) {
156
+ const { apiKey } = JOB_NIMBUS_SECRET_SCHEMA.parse(secret)
157
+ const response = await fetch(
158
+ new URL(
159
+ encodeURIComponent(z.string().trim().min(1).parse(fileId)),
160
+ "https://app.jobnimbus.com/files/",
161
+ ),
162
+ { headers: { Authorization: `Bearer ${apiKey}` } },
163
+ )
164
+ if (!response.ok) {
165
+ throw new JobNimbusApiError(response.status, {
166
+ response: await response.text(),
167
+ })
168
+ }
169
+ return response.blob()
170
+ }
171
+
172
+ /**
173
+ * Converts public camelCase JSON into JobNimbus's provider-native keys.
174
+ *
175
+ * @param value - Codec-safe public value.
176
+ */
177
+ export function toJobNimbus(value: Encodable): Encodable {
178
+ if (value instanceof Date) return value.toISOString()
179
+ if (Array.isArray(value)) return value.map(toJobNimbus)
180
+ if (!isPlainObject(value)) return value
181
+
182
+ return Object.fromEntries(
183
+ Object.entries(value).flatMap(([key, item]) =>
184
+ key === "customFields" && isPlainObject(item)
185
+ ? Object.entries(item).map(([field, fieldValue]) => [
186
+ field,
187
+ toJobNimbus(fieldValue),
188
+ ])
189
+ : [[toSnakeCase(key), toJobNimbus(item)]],
190
+ ),
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Converts JobNimbus JSON into public camelCase values.
196
+ *
197
+ * Documented legacy Unix date fields become Date instances. Unknown fields are
198
+ * preserved so account-defined CRM fields survive normalization.
199
+ *
200
+ * @param value - Raw provider value.
201
+ */
202
+ export function fromJobNimbus(value: unknown): Encodable {
203
+ return normalizeJobNimbusValue(value)
204
+ }
205
+
206
+ /**
207
+ * Recursively normalizes one raw provider value and its original key.
208
+ *
209
+ * @param value - Raw provider value.
210
+ * @param providerKey - Original provider field name, when nested in an object.
211
+ */
212
+ function normalizeJobNimbusValue(
213
+ value: unknown,
214
+ providerKey?: string,
215
+ ): Encodable {
216
+ if (typeof value === "number" && providerKey?.startsWith("date_")) {
217
+ return new Date(value * 1_000)
218
+ }
219
+ if (Array.isArray(value)) {
220
+ return value.map((item) => normalizeJobNimbusValue(item))
221
+ }
222
+ if (!isPlainObject(value)) return JOB_NIMBUS_VALUE_SCHEMA.parse(value)
223
+
224
+ const customFields: Record<string, Encodable> = {}
225
+ return Object.fromEntries([
226
+ ...Object.entries(value).flatMap(([key, item]) => {
227
+ if (/^cf_(?:boolean|date|double|long|string)_\d+$/.test(key)) {
228
+ customFields[key] = normalizeJobNimbusValue(item, key)
229
+ return []
230
+ }
231
+ return [
232
+ [
233
+ key === "extermal_id"
234
+ ? "externalId"
235
+ : key === "jnid"
236
+ ? "id"
237
+ : key === "recid"
238
+ ? "recordId"
239
+ : toCamelCase(key),
240
+ normalizeJobNimbusValue(item, key),
241
+ ] as const,
242
+ ]
243
+ }),
244
+ ...(Object.keys(customFields).length > 0
245
+ ? [["customFields", customFields] as const]
246
+ : []),
247
+ ])
248
+ }
249
+
250
+ /**
251
+ * Parses a response body while preserving non-JSON diagnostics.
252
+ *
253
+ * @param value - Raw response body.
254
+ */
255
+ function parseJson(value: string): unknown {
256
+ try {
257
+ return JSON.parse(value)
258
+ } catch {
259
+ return { response: value }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Checks whether a value can be traversed as a JSON-style object.
265
+ *
266
+ * @param value - Candidate object value.
267
+ */
268
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
269
+ return typeof value === "object" && value !== null && !Array.isArray(value)
270
+ }
271
+
272
+ /**
273
+ * Converts one provider snake-case key to camel case.
274
+ *
275
+ * @param value - Provider field name.
276
+ */
277
+ function toCamelCase(value: string) {
278
+ return value
279
+ .replace(/_([a-z0-9])/g, (_match, character: string) =>
280
+ character.toUpperCase(),
281
+ )
282
+ .replace(/^[A-Z]/, (character) => character.toLowerCase())
283
+ }
284
+
285
+ /**
286
+ * Converts one public camel-case key to provider snake case.
287
+ *
288
+ * @param value - Public field name.
289
+ */
290
+ function toSnakeCase(value: string) {
291
+ return value
292
+ .replace(/([a-zA-Z])([0-9])/g, "$1_$2")
293
+ .replace(/[A-Z]/g, (character) => `_${character.toLowerCase()}`)
294
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./api"
2
+ export * from "./schemas"