@automate.ax/integration-contracts 0.116.0 → 0.118.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,121 @@
1
+ import * as z from "zod";
2
+ export declare const TIDYCAL_ACCOUNT_SCHEMA: z.ZodObject<{
3
+ currencySymbol: z.ZodString;
4
+ email: z.ZodEmail;
5
+ language: z.ZodString;
6
+ lifetimeProAt: z.ZodNullable<z.ZodISODateTime>;
7
+ name: z.ZodString;
8
+ profilePictureUrl: z.ZodNullable<z.ZodURL>;
9
+ vanityPath: z.ZodString;
10
+ }, z.core.$strip>;
11
+ export declare const TIDYCAL_CONTACT_SCHEMA: z.ZodObject<{
12
+ createdAt: z.ZodISODateTime;
13
+ email: z.ZodEmail;
14
+ id: z.ZodNumber;
15
+ ipAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
16
+ name: z.ZodString;
17
+ phoneNumber: z.ZodOptional<z.ZodNullable<z.ZodString>>;
18
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
+ updatedAt: z.ZodISODateTime;
20
+ }, z.core.$strip>;
21
+ export declare const TIDYCAL_PAYMENT_SCHEMA: z.ZodObject<{
22
+ amount: z.ZodNumber;
23
+ bookingId: z.ZodNumber;
24
+ createdAt: z.ZodISODateTime;
25
+ currency: z.ZodString;
26
+ id: z.ZodNumber;
27
+ paymentId: z.ZodString;
28
+ updatedAt: z.ZodISODateTime;
29
+ }, z.core.$strip>;
30
+ export declare const TIDYCAL_QUESTION_SCHEMA: z.ZodObject<{
31
+ answer: z.ZodString;
32
+ bookingId: z.ZodNumber;
33
+ createdAt: z.ZodISODateTime;
34
+ id: z.ZodNumber;
35
+ question: z.ZodString;
36
+ updatedAt: z.ZodISODateTime;
37
+ }, z.core.$strip>;
38
+ export declare const TIDYCAL_BOOKING_SCHEMA: z.ZodObject<{
39
+ bookingTypeId: z.ZodNumber;
40
+ cancelledAt: z.ZodNullable<z.ZodISODateTime>;
41
+ contact: z.ZodObject<{
42
+ createdAt: z.ZodISODateTime;
43
+ email: z.ZodEmail;
44
+ id: z.ZodNumber;
45
+ ipAddress: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
+ name: z.ZodString;
47
+ phoneNumber: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ updatedAt: z.ZodISODateTime;
50
+ }, z.core.$strip>;
51
+ contactId: z.ZodNumber;
52
+ createdAt: z.ZodISODateTime;
53
+ endsAt: z.ZodISODateTime;
54
+ id: z.ZodNumber;
55
+ meetingId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
56
+ meetingUrl: z.ZodOptional<z.ZodNullable<z.ZodURL>>;
57
+ payment: z.ZodOptional<z.ZodNullable<z.ZodObject<{
58
+ amount: z.ZodNumber;
59
+ bookingId: z.ZodNumber;
60
+ createdAt: z.ZodISODateTime;
61
+ currency: z.ZodString;
62
+ id: z.ZodNumber;
63
+ paymentId: z.ZodString;
64
+ updatedAt: z.ZodISODateTime;
65
+ }, z.core.$strip>>>;
66
+ questions: z.ZodArray<z.ZodObject<{
67
+ answer: z.ZodString;
68
+ bookingId: z.ZodNumber;
69
+ createdAt: z.ZodISODateTime;
70
+ id: z.ZodNumber;
71
+ question: z.ZodString;
72
+ updatedAt: z.ZodISODateTime;
73
+ }, z.core.$strip>>;
74
+ startsAt: z.ZodISODateTime;
75
+ timezone: z.ZodString;
76
+ updatedAt: z.ZodISODateTime;
77
+ }, z.core.$strip>;
78
+ export declare const TIDYCAL_BOOKING_TYPE_SCHEMA: z.ZodObject<{
79
+ bookingThresholdMinutes: z.ZodNumber;
80
+ createdAt: z.ZodISODateTime;
81
+ currencyCode: z.ZodOptional<z.ZodNullable<z.ZodString>>;
82
+ description: z.ZodString;
83
+ disabledAt: z.ZodNullable<z.ZodISODateTime>;
84
+ durationMinutes: z.ZodNumber;
85
+ id: z.ZodNumber;
86
+ latestAvailabilityDays: z.ZodNumber;
87
+ maxBookings: z.ZodNumber;
88
+ paddingMinutes: z.ZodNumber;
89
+ paymentPlatform: z.ZodNullable<z.ZodEnum<{
90
+ tidycal: "tidycal";
91
+ paypal: "paypal";
92
+ stripe: "stripe";
93
+ }>>;
94
+ paymentPlatformId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
95
+ price: z.ZodNumber;
96
+ private: z.ZodBoolean;
97
+ redirectUrl: z.ZodNullable<z.ZodURL>;
98
+ title: z.ZodString;
99
+ updatedAt: z.ZodISODateTime;
100
+ url: z.ZodURL;
101
+ urlSlug: z.ZodString;
102
+ userId: z.ZodNumber;
103
+ }, z.core.$strip>;
104
+ export declare const TIDYCAL_TIMESLOT_SCHEMA: z.ZodObject<{
105
+ availableBookings: z.ZodNumber;
106
+ endsAt: z.ZodISODateTime;
107
+ startsAt: z.ZodISODateTime;
108
+ }, z.core.$strip>;
109
+ export declare const TIDYCAL_TEAM_SCHEMA: z.ZodObject<{
110
+ createdAt: z.ZodISODateTime;
111
+ id: z.ZodNumber;
112
+ name: z.ZodString;
113
+ updatedAt: z.ZodISODateTime;
114
+ }, z.core.$strip>;
115
+ export declare const TIDYCAL_TEAM_USER_SCHEMA: z.ZodObject<{
116
+ createdAt: z.ZodISODateTime;
117
+ email: z.ZodEmail;
118
+ id: z.ZodNumber;
119
+ name: z.ZodString;
120
+ updatedAt: z.ZodISODateTime;
121
+ }, z.core.$strip>;
@@ -0,0 +1,95 @@
1
+ import * as z from "zod";
2
+ const DATE_TIME_SCHEMA = z.iso.datetime({ offset: true });
3
+ const ID_SCHEMA = z.number().int().positive();
4
+ export const TIDYCAL_ACCOUNT_SCHEMA = z.object({
5
+ currencySymbol: z.string(),
6
+ email: z.email(),
7
+ language: z.string(),
8
+ lifetimeProAt: DATE_TIME_SCHEMA.nullable(),
9
+ name: z.string(),
10
+ profilePictureUrl: z.url().nullable(),
11
+ vanityPath: z.string(),
12
+ });
13
+ export const TIDYCAL_CONTACT_SCHEMA = z.object({
14
+ createdAt: DATE_TIME_SCHEMA,
15
+ email: z.email(),
16
+ id: ID_SCHEMA,
17
+ ipAddress: z.string().nullable().optional(),
18
+ name: z.string(),
19
+ phoneNumber: z.string().nullable().optional(),
20
+ timezone: z.string().nullable().optional(),
21
+ updatedAt: DATE_TIME_SCHEMA,
22
+ });
23
+ export const TIDYCAL_PAYMENT_SCHEMA = z.object({
24
+ amount: z.number().nonnegative(),
25
+ bookingId: ID_SCHEMA,
26
+ createdAt: DATE_TIME_SCHEMA,
27
+ currency: z.string(),
28
+ id: ID_SCHEMA,
29
+ paymentId: z.string(),
30
+ updatedAt: DATE_TIME_SCHEMA,
31
+ });
32
+ export const TIDYCAL_QUESTION_SCHEMA = z.object({
33
+ answer: z.string(),
34
+ bookingId: ID_SCHEMA,
35
+ createdAt: DATE_TIME_SCHEMA,
36
+ id: ID_SCHEMA,
37
+ question: z.string(),
38
+ updatedAt: DATE_TIME_SCHEMA,
39
+ });
40
+ export const TIDYCAL_BOOKING_SCHEMA = z.object({
41
+ bookingTypeId: ID_SCHEMA,
42
+ cancelledAt: DATE_TIME_SCHEMA.nullable(),
43
+ contact: TIDYCAL_CONTACT_SCHEMA,
44
+ contactId: ID_SCHEMA,
45
+ createdAt: DATE_TIME_SCHEMA,
46
+ endsAt: DATE_TIME_SCHEMA,
47
+ id: ID_SCHEMA,
48
+ meetingId: z.string().nullable().optional(),
49
+ meetingUrl: z.url().nullable().optional(),
50
+ payment: TIDYCAL_PAYMENT_SCHEMA.nullable().optional(),
51
+ questions: TIDYCAL_QUESTION_SCHEMA.array(),
52
+ startsAt: DATE_TIME_SCHEMA,
53
+ timezone: z.string(),
54
+ updatedAt: DATE_TIME_SCHEMA,
55
+ });
56
+ export const TIDYCAL_BOOKING_TYPE_SCHEMA = z.object({
57
+ bookingThresholdMinutes: z.number().int().min(0),
58
+ createdAt: DATE_TIME_SCHEMA,
59
+ currencyCode: z.string().nullable().optional(),
60
+ description: z.string(),
61
+ disabledAt: DATE_TIME_SCHEMA.nullable(),
62
+ durationMinutes: z.number().int().positive(),
63
+ id: ID_SCHEMA,
64
+ latestAvailabilityDays: z.number().int().min(0),
65
+ maxBookings: z.number().int().positive(),
66
+ paddingMinutes: z.number().int().min(0),
67
+ paymentPlatform: z.enum(["paypal", "stripe", "tidycal"]).nullable(),
68
+ paymentPlatformId: z.string().nullable().optional(),
69
+ price: z.number().nonnegative(),
70
+ private: z.boolean(),
71
+ redirectUrl: z.url().nullable(),
72
+ title: z.string(),
73
+ updatedAt: DATE_TIME_SCHEMA,
74
+ url: z.url(),
75
+ urlSlug: z.string(),
76
+ userId: ID_SCHEMA,
77
+ });
78
+ export const TIDYCAL_TIMESLOT_SCHEMA = z.object({
79
+ availableBookings: z.number().int().nonnegative(),
80
+ endsAt: DATE_TIME_SCHEMA,
81
+ startsAt: DATE_TIME_SCHEMA,
82
+ });
83
+ export const TIDYCAL_TEAM_SCHEMA = z.object({
84
+ createdAt: DATE_TIME_SCHEMA,
85
+ id: ID_SCHEMA,
86
+ name: z.string(),
87
+ updatedAt: DATE_TIME_SCHEMA,
88
+ });
89
+ export const TIDYCAL_TEAM_USER_SCHEMA = z.object({
90
+ createdAt: DATE_TIME_SCHEMA,
91
+ email: z.email(),
92
+ id: ID_SCHEMA,
93
+ name: z.string(),
94
+ updatedAt: DATE_TIME_SCHEMA,
95
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/integration-contracts",
3
- "version": "0.116.0",
3
+ "version": "0.118.0",
4
4
  "description": "Shared integration payload contracts and provider primitives for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,7 @@
40
40
  "./slack": "./src/slack/index.ts",
41
41
  "./stripe": "./src/stripe/index.ts",
42
42
  "./teams": "./src/teams/index.ts",
43
+ "./tidycal": "./src/tidycal/index.ts",
43
44
  "./trello": "./src/trello/index.ts",
44
45
  "./triggers": "./src/triggers.ts",
45
46
  "./vercel": "./src/vercel/index.ts",
@@ -52,7 +53,7 @@
52
53
  }
53
54
  },
54
55
  "dependencies": {
55
- "@automate.ax/codec": "0.116.0",
56
+ "@automate.ax/codec": "0.118.0",
56
57
  "@cfworker/json-schema": "^4.1.1",
57
58
  "@googleapis/calendar": "^15.0.0",
58
59
  "@googleapis/forms": "^6.0.1",
@@ -213,6 +214,11 @@
213
214
  "types": "./dist/teams/index.d.ts",
214
215
  "default": "./dist/teams/index.js"
215
216
  },
217
+ "./tidycal": {
218
+ "bun": "./src/tidycal/index.ts",
219
+ "types": "./dist/tidycal/index.d.ts",
220
+ "default": "./dist/tidycal/index.js"
221
+ },
216
222
  "./trello": {
217
223
  "bun": "./src/trello/index.ts",
218
224
  "types": "./dist/trello/index.d.ts",
@@ -1,5 +1,10 @@
1
1
  import * as z from "zod"
2
- import { LABEL_CHANGE_SCHEMA, MESSAGE_SCHEMA } from "./schemas"
2
+ import {
3
+ LABEL_CHANGE_SCHEMA,
4
+ MAILBOX_CHANGE_SCHEMA,
5
+ MESSAGE_DELETED_SCHEMA,
6
+ MESSAGE_SCHEMA,
7
+ } from "./schemas"
3
8
 
4
9
  export * from "./gmail"
5
10
  export * from "./schemas"
@@ -15,6 +20,18 @@ export const gmailTriggerContracts = {
15
20
  configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
16
21
  eventSchema: LABEL_CHANGE_SCHEMA,
17
22
  },
23
+ "gmail.mailbox.changed": {
24
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
25
+ eventSchema: MAILBOX_CHANGE_SCHEMA,
26
+ },
27
+ "gmail.message.added": {
28
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
29
+ eventSchema: MESSAGE_SCHEMA,
30
+ },
31
+ "gmail.message.deleted": {
32
+ configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
33
+ eventSchema: MESSAGE_DELETED_SCHEMA,
34
+ },
18
35
  "gmail.message.received": {
19
36
  configSchema: GMAIL_TRIGGER_CONFIG_SCHEMA,
20
37
  eventSchema: MESSAGE_SCHEMA,
@@ -363,6 +363,32 @@ export const LABEL_CHANGE_SCHEMA = z.object({
363
363
  message: MESSAGE_SCHEMA,
364
364
  })
365
365
 
366
+ /** Stable identity retained after Gmail permanently deletes a message. */
367
+ export const MESSAGE_DELETED_SCHEMA = MESSAGE_IDENTIFIER_SCHEMA.extend({
368
+ /** Mailbox history record that reported the deletion. */
369
+ historyId: z.string(),
370
+ })
371
+
372
+ /** Broad normalized Gmail mailbox-history change. */
373
+ export const MAILBOX_CHANGE_SCHEMA = z.discriminatedUnion("changeType", [
374
+ z.object({
375
+ changeType: z.literal("messageAdded"),
376
+ historyId: z.string(),
377
+ message: MESSAGE_SCHEMA,
378
+ }),
379
+ MESSAGE_DELETED_SCHEMA.extend({
380
+ changeType: z.literal("messageDeleted"),
381
+ }),
382
+ LABEL_CHANGE_SCHEMA.extend({
383
+ changeType: z.literal("labelAdded"),
384
+ historyId: z.string(),
385
+ }),
386
+ LABEL_CHANGE_SCHEMA.extend({
387
+ changeType: z.literal("labelRemoved"),
388
+ historyId: z.string(),
389
+ }),
390
+ ])
391
+
366
392
  export const THREAD_IDENTIFIER_SCHEMA = z.object({
367
393
  /** Immutable Gmail thread ID. */
368
394
  threadId: z.string(),
@@ -14,3 +14,21 @@ export const GOOGLE_CALENDAR_EVENT_DELETED_SCHEMA = z.object({
14
14
  /** Immutable provider event ID. */
15
15
  eventId: z.string(),
16
16
  })
17
+
18
+ /** Broad normalized Google Calendar event lifecycle change. */
19
+ export const GOOGLE_CALENDAR_EVENT_CHANGED_SCHEMA = z.discriminatedUnion(
20
+ "changeType",
21
+ [
22
+ z.object({
23
+ changeType: z.literal("created"),
24
+ event: CALENDAR_EVENT_SCHEMA,
25
+ }),
26
+ z.object({
27
+ changeType: z.literal("updated"),
28
+ event: CALENDAR_EVENT_SCHEMA,
29
+ }),
30
+ GOOGLE_CALENDAR_EVENT_DELETED_SCHEMA.extend({
31
+ changeType: z.literal("deleted"),
32
+ }),
33
+ ],
34
+ )
@@ -1,5 +1,8 @@
1
1
  import * as z from "zod"
2
- import { GOOGLE_CALENDAR_EVENT_DELETED_SCHEMA } from "./event-schemas"
2
+ import {
3
+ GOOGLE_CALENDAR_EVENT_CHANGED_SCHEMA,
4
+ GOOGLE_CALENDAR_EVENT_DELETED_SCHEMA,
5
+ } from "./event-schemas"
3
6
  import { CALENDAR_EVENT_SCHEMA } from "./schemas"
4
7
 
5
8
  export * from "./event-schemas"
@@ -12,6 +15,10 @@ export const GOOGLE_CALENDAR_TRIGGER_CONFIG_SCHEMA = z.object({
12
15
  })
13
16
 
14
17
  export const googleCalendarTriggerContracts = {
18
+ "googleCalendar.event.changed": {
19
+ configSchema: GOOGLE_CALENDAR_TRIGGER_CONFIG_SCHEMA,
20
+ eventSchema: GOOGLE_CALENDAR_EVENT_CHANGED_SCHEMA,
21
+ },
15
22
  "googleCalendar.event.created": {
16
23
  configSchema: GOOGLE_CALENDAR_TRIGGER_CONFIG_SCHEMA,
17
24
  eventSchema: CALENDAR_EVENT_SCHEMA,
@@ -0,0 +1,239 @@
1
+ import type { Encodable } from "@automate.ax/codec"
2
+ import { encodableSchema } from "@automate.ax/codec"
3
+ import * as z from "zod"
4
+
5
+ const TIDYCAL_API_BASE_URL = "https://tidycal.com/api/"
6
+ const TIDYCAL_API_ORIGIN = new URL(TIDYCAL_API_BASE_URL).origin
7
+ const TIDYCAL_PERSONAL_ACCESS_TOKEN_SECRET_SCHEMA = z.object({
8
+ apiKey: z.string().min(1),
9
+ })
10
+ const TIDYCAL_OAUTH_SECRET_SCHEMA = z.object({
11
+ accessToken: z.string().min(1),
12
+ })
13
+
14
+ /** Resolved TidyCal account accepted by the shared API client. */
15
+ export interface TidyCalResolvedAccount {
16
+ connectionMethodId: string
17
+ secret: Record<string, unknown>
18
+ serviceId: "tidycal"
19
+ }
20
+
21
+ /** Options for one authenticated TidyCal REST request. */
22
+ export interface TidyCalRequestOptions<TSchema extends z.ZodType> {
23
+ /** Public camelCase JSON body. */
24
+ body?: Encodable
25
+
26
+ /** HTTP verb. Defaults to `GET`. */
27
+ method?: "DELETE" | "GET" | "PATCH" | "POST"
28
+
29
+ /** Public camelCase query parameters. */
30
+ query?: Record<string, boolean | number | string | undefined>
31
+
32
+ /** Schema for the normalized provider response. */
33
+ responseSchema: TSchema
34
+ }
35
+
36
+ /** Structured TidyCal REST error. */
37
+ export class TidyCalApiError extends Error {
38
+ /** Normalized provider error payload, when it was valid JSON. */
39
+ readonly body?: Encodable
40
+
41
+ /** Retry delay in seconds, when TidyCal returned one. */
42
+ readonly retryAfter?: number
43
+
44
+ /** HTTP status returned by TidyCal. */
45
+ readonly status: number
46
+
47
+ /**
48
+ * Creates a structured error from one TidyCal response.
49
+ *
50
+ * @param options - Response status, provider body, and retry metadata.
51
+ * @param options.body - Normalized provider error payload.
52
+ * @param options.retryAfter - Retry delay in seconds.
53
+ * @param options.status - HTTP response status.
54
+ */
55
+ constructor(options: {
56
+ body?: Encodable
57
+ retryAfter?: number
58
+ status: number
59
+ }) {
60
+ super(
61
+ getErrorMessage(options.body) ??
62
+ `TidyCal API request failed with status ${options.status}.`,
63
+ )
64
+ this.name = "TidyCalApiError"
65
+ this.body = options.body
66
+ this.retryAfter = options.retryAfter
67
+ this.status = options.status
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Creates an authenticated TidyCal REST client.
73
+ *
74
+ * @param account - Resolved OAuth or personal-access-token account.
75
+ * @throws When the connection method or secret shape is unsupported.
76
+ */
77
+ export function getTidyCalApi(account: TidyCalResolvedAccount) {
78
+ const accessToken =
79
+ account.connectionMethodId === "oauth"
80
+ ? TIDYCAL_OAUTH_SECRET_SCHEMA.parse(account.secret).accessToken
81
+ : account.connectionMethodId === "personal-access-token"
82
+ ? TIDYCAL_PERSONAL_ACCESS_TOKEN_SECRET_SCHEMA.parse(account.secret)
83
+ .apiKey
84
+ : undefined
85
+ if (!accessToken) {
86
+ throw new Error(
87
+ `Unsupported TidyCal connection method: ${account.connectionMethodId}`,
88
+ )
89
+ }
90
+
91
+ /**
92
+ * Runs one request and returns its normalized response.
93
+ *
94
+ * @param path - TidyCal API path relative to the API root.
95
+ * @param options - Method, parameters, and response schema.
96
+ */
97
+ async function request<TSchema extends z.ZodType>(
98
+ path: string,
99
+ options: TidyCalRequestOptions<TSchema>,
100
+ ): Promise<z.output<TSchema>> {
101
+ const url = new URL(path.replace(/^\//, ""), TIDYCAL_API_BASE_URL)
102
+ if (url.origin !== TIDYCAL_API_ORIGIN) {
103
+ throw new Error("TidyCal API paths must use the TidyCal API origin.")
104
+ }
105
+ for (const [name, value] of Object.entries(options.query ?? {})) {
106
+ if (value !== undefined)
107
+ url.searchParams.set(toSnakeCase(name), String(value))
108
+ }
109
+ const response = await fetch(url, {
110
+ body:
111
+ options.body === undefined
112
+ ? undefined
113
+ : JSON.stringify(toTidyCal(options.body)),
114
+ headers: {
115
+ Accept: "application/json",
116
+ Authorization: `Bearer ${accessToken}`,
117
+ ...(options.body === undefined
118
+ ? {}
119
+ : { "Content-Type": "application/json" }),
120
+ },
121
+ method: options.method ?? "GET",
122
+ })
123
+ const normalized = fromTidyCal(parseJson(await response.text()))
124
+ if (!response.ok) {
125
+ const body = encodableSchema.safeParse(normalized)
126
+ throw new TidyCalApiError({
127
+ ...(body.success && { body: body.data }),
128
+ retryAfter: parseFiniteNumber(response.headers.get("Retry-After")),
129
+ status: response.status,
130
+ })
131
+ }
132
+ return options.responseSchema.parse(normalized)
133
+ }
134
+
135
+ return { request }
136
+ }
137
+
138
+ /**
139
+ * Converts public camelCase JSON to TidyCal wire keys.
140
+ *
141
+ * @param value - Codec-safe public value.
142
+ */
143
+ export function toTidyCal(value: Encodable): Encodable {
144
+ if (value instanceof Date) return value.toISOString()
145
+ if (Array.isArray(value)) return value.map(toTidyCal)
146
+ if (!isPlainObject(value)) return value
147
+ return Object.fromEntries(
148
+ Object.entries(value).map(([key, item]) => [
149
+ toSnakeCase(key),
150
+ toTidyCal(item),
151
+ ]),
152
+ )
153
+ }
154
+
155
+ /**
156
+ * Converts TidyCal wire JSON to public camelCase keys.
157
+ *
158
+ * @param value - Untrusted provider JSON value.
159
+ */
160
+ export function fromTidyCal(value: unknown): unknown {
161
+ if (Array.isArray(value)) return value.map(fromTidyCal)
162
+ if (!isPlainObject(value)) return value
163
+ return Object.fromEntries(
164
+ Object.entries(value).map(([key, item]) => [
165
+ toCamelCase(key),
166
+ fromTidyCal(item),
167
+ ]),
168
+ )
169
+ }
170
+
171
+ /**
172
+ * Returns whether a value is a plain JSON object.
173
+ *
174
+ * @param value - Value to inspect.
175
+ */
176
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
177
+ return typeof value === "object" && value !== null && !Array.isArray(value)
178
+ }
179
+
180
+ /**
181
+ * Parses JSON without obscuring the eventual structured API error.
182
+ *
183
+ * @param value - Response text to parse.
184
+ */
185
+ function parseJson(value: string): unknown {
186
+ try {
187
+ return JSON.parse(value)
188
+ } catch {
189
+ return undefined
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Parses a finite numeric response header.
195
+ *
196
+ * @param value - Header value.
197
+ */
198
+ function parseFiniteNumber(value: string | null) {
199
+ if (value === null) return undefined
200
+ const number = Number(value)
201
+ return Number.isFinite(number) ? number : undefined
202
+ }
203
+
204
+ /**
205
+ * Extracts the provider's primary error text without weakening its payload.
206
+ *
207
+ * @param body - Normalized provider error body.
208
+ */
209
+ function getErrorMessage(body: Encodable | undefined) {
210
+ if (!isPlainObject(body)) return undefined
211
+ if (typeof body.message === "string") return body.message
212
+ const errors = body.errors
213
+ if (!isPlainObject(errors)) return undefined
214
+ const first = Object.values(errors)[0]
215
+ if (typeof first === "string") return first
216
+ return Array.isArray(first) && typeof first[0] === "string"
217
+ ? first[0]
218
+ : undefined
219
+ }
220
+
221
+ /**
222
+ * Converts one camelCase public key to snake_case.
223
+ *
224
+ * @param value - Public key.
225
+ */
226
+ function toSnakeCase(value: string) {
227
+ return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
228
+ }
229
+
230
+ /**
231
+ * Converts one snake_case provider key to camelCase.
232
+ *
233
+ * @param value - Provider key.
234
+ */
235
+ function toCamelCase(value: string) {
236
+ return value.replace(/_([a-z0-9])/g, (_, letter: string) =>
237
+ letter.toUpperCase(),
238
+ )
239
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./api"
2
+ export * from "./schemas"