@automate.ax/integration-contracts 0.146.1 → 0.147.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 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { Encodable } from "@automate.ax/codec";
2
+ import * as z from "zod";
3
+ import type { components } from "./openapi-types.generated";
4
+ type OpenApiSchemas = components["schemas"];
5
+ export type TallySchemaName = keyof OpenApiSchemas;
6
+ type TallyEncodable<TValue> = unknown extends TValue ? Encodable : TValue extends null | undefined | boolean | number | string ? TValue : TValue extends readonly (infer TItem)[] ? TallyEncodable<TItem>[] : TValue extends object ? keyof TValue extends never ? Record<string, Encodable> : {
7
+ [TKey in keyof TValue]: TallyEncodable<TValue[TKey]>;
8
+ } : never;
9
+ /** Public JSON-safe value for one generated Tally OpenAPI schema. */
10
+ export type TallySchemaValue<TName extends TallySchemaName> = TallyEncodable<OpenApiSchemas[TName]>;
11
+ /**
12
+ * Validates one value against the frozen Tally OpenAPI schema.
13
+ *
14
+ * @param name OpenAPI schema name.
15
+ */
16
+ export declare function tallySchema<TName extends TallySchemaName>(name: TName): z.ZodType<TallySchemaValue<TName>, TallySchemaValue<TName>>;
17
+ export {};
@@ -0,0 +1,23 @@
1
+ import { Validator } from "@cfworker/json-schema";
2
+ import * as z from "zod";
3
+ import schemaData from "./openapi-schemas.generated.json" with { type: "json" };
4
+ const VALIDATOR = new Validator({
5
+ $defs: schemaData.definitions,
6
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7
+ additionalProperties: false,
8
+ maxProperties: 1,
9
+ minProperties: 1,
10
+ properties: Object.fromEntries(Object.keys(schemaData.definitions).map((name) => [
11
+ name,
12
+ { $ref: `#/$defs/${name}` },
13
+ ])),
14
+ type: "object",
15
+ }, "2020-12");
16
+ /**
17
+ * Validates one value against the frozen Tally OpenAPI schema.
18
+ *
19
+ * @param name OpenAPI schema name.
20
+ */
21
+ export function tallySchema(name) {
22
+ return z.custom((value) => VALIDATOR.validate({ [name]: value }).valid);
23
+ }
@@ -32,12 +32,13 @@ import type { resendTriggerContracts } from "./resend/index.js";
32
32
  import type { slackTriggerContracts } from "./slack/index.js";
33
33
  import type { stripeTriggerContracts } from "./stripe/index.js";
34
34
  import type { teamsTriggerContracts } from "./teams/index.js";
35
+ import type { tallyTriggerContracts } from "./tally/index.js";
35
36
  import type { trelloTriggerContracts } from "./trello/index.js";
36
37
  import type { vercelTriggerContracts } from "./vercel/index.js";
37
38
  import type { webflowTriggerContracts } from "./webflow/index.js";
38
39
  import type { whatsappTriggerContracts } from "./whatsapp/index.js";
39
40
  import type { z } from "zod";
40
- export type TriggerContractMap = typeof airtableTriggerContracts & typeof anthropicTriggerContracts & typeof apifyTriggerContracts & typeof axiomTriggerContracts & typeof automateTriggerContracts & typeof asanaTriggerContracts & typeof brevoTriggerContracts & typeof calcomTriggerContracts & typeof convexTriggerContracts & typeof discordTriggerContracts & typeof closeTriggerContracts & typeof clickUpTriggerContracts & typeof calendlyTriggerContracts & typeof cloudflareTriggerContracts & typeof githubTriggerContracts & typeof gmailTriggerContracts & typeof googleCalendarTriggerContracts & typeof googleDriveTriggerContracts & typeof googleFormsTriggerContracts & typeof googleMeetTriggerContracts & typeof googleAdsTriggerContracts & typeof googleSheetsTriggerContracts & typeof hubspotTriggerContracts & typeof highLevelTriggerContracts & typeof linearTriggerContracts & typeof millionVerifierTriggerContracts & typeof metaAdsTriggerContracts & typeof notionTriggerContracts & typeof outlookTriggerContracts & typeof redditTriggerContracts & typeof resendTriggerContracts & typeof slackTriggerContracts & typeof stripeTriggerContracts & typeof teamsTriggerContracts & typeof trelloTriggerContracts & typeof vercelTriggerContracts & typeof webflowTriggerContracts & typeof whatsappTriggerContracts;
41
+ export type TriggerContractMap = typeof airtableTriggerContracts & typeof anthropicTriggerContracts & typeof apifyTriggerContracts & typeof axiomTriggerContracts & typeof automateTriggerContracts & typeof asanaTriggerContracts & typeof brevoTriggerContracts & typeof calcomTriggerContracts & typeof convexTriggerContracts & typeof discordTriggerContracts & typeof closeTriggerContracts & typeof clickUpTriggerContracts & typeof calendlyTriggerContracts & typeof cloudflareTriggerContracts & typeof githubTriggerContracts & typeof gmailTriggerContracts & typeof googleCalendarTriggerContracts & typeof googleDriveTriggerContracts & typeof googleFormsTriggerContracts & typeof googleMeetTriggerContracts & typeof googleAdsTriggerContracts & typeof googleSheetsTriggerContracts & typeof hubspotTriggerContracts & typeof highLevelTriggerContracts & typeof linearTriggerContracts & typeof millionVerifierTriggerContracts & typeof metaAdsTriggerContracts & typeof notionTriggerContracts & typeof outlookTriggerContracts & typeof redditTriggerContracts & typeof resendTriggerContracts & typeof slackTriggerContracts & typeof stripeTriggerContracts & typeof teamsTriggerContracts & typeof tallyTriggerContracts & typeof trelloTriggerContracts & typeof vercelTriggerContracts & typeof webflowTriggerContracts & typeof whatsappTriggerContracts;
41
42
  export type IntegrationTriggerType = keyof TriggerContractMap;
42
43
  /** Canonical authoring configuration for one integration trigger type. */
43
44
  export type TriggerConfig<TType extends IntegrationTriggerType> = z.input<TriggerContractMap[TType]["configSchema"]> extends Record<string, never> ? object : z.input<TriggerContractMap[TType]["configSchema"]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/integration-contracts",
3
- "version": "0.146.1",
3
+ "version": "0.147.0",
4
4
  "description": "Shared integration payload contracts and provider primitives for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -54,6 +54,7 @@
54
54
  "./slack": "./src/slack/index.ts",
55
55
  "./stripe": "./src/stripe/index.ts",
56
56
  "./teams": "./src/teams/index.ts",
57
+ "./tally": "./src/tally/index.ts",
57
58
  "./tidycal": "./src/tidycal/index.ts",
58
59
  "./trello": "./src/trello/index.ts",
59
60
  "./triggers": "./src/triggers.ts",
@@ -68,7 +69,7 @@
68
69
  },
69
70
  "dependencies": {
70
71
  "@anthropic-ai/sdk": "0.123.0",
71
- "@automate.ax/codec": "0.146.1",
72
+ "@automate.ax/codec": "0.147.0",
72
73
  "@cfworker/json-schema": "^4.1.1",
73
74
  "@googleapis/calendar": "^16.0.0",
74
75
  "@googleapis/forms": "^6.0.1",
@@ -105,6 +106,7 @@
105
106
  "generate:github": "bun scripts/generate-github-webhook-schemas.ts",
106
107
  "generate:calcom": "bun scripts/generate-calcom-openapi.ts",
107
108
  "generate:highlevel": "bun scripts/generate-highlevel-openapi.ts",
109
+ "generate:tally": "bun scripts/generate-tally-openapi.ts",
108
110
  "typecheck": "resource-broker run --pool automate-ax-validation --limit 2 --weight 1 -- tsc --noEmit",
109
111
  "lint": "oxlint --type-aware --threads=2 && bun --bun eslint . --cache --cache-strategy content",
110
112
  "lint:fix": "oxlint --type-aware --threads=2 --fix --fix-suggestions && bun --bun eslint . --fix --cache --cache-strategy content",
@@ -303,6 +305,11 @@
303
305
  "types": "./dist/teams/index.d.ts",
304
306
  "default": "./dist/teams/index.js"
305
307
  },
308
+ "./tally": {
309
+ "bun": "./src/tally/index.ts",
310
+ "types": "./dist/tally/index.d.ts",
311
+ "default": "./dist/tally/index.js"
312
+ },
306
313
  "./tidycal": {
307
314
  "bun": "./src/tidycal/index.ts",
308
315
  "types": "./dist/tidycal/index.d.ts",
@@ -0,0 +1,233 @@
1
+ import type { Encodable } from "@automate.ax/codec"
2
+ import { encodableSchema } from "@automate.ax/codec"
3
+ import * as z from "zod"
4
+ import {
5
+ tallySchema,
6
+ type TallySchemaName,
7
+ type TallySchemaValue,
8
+ } from "./schemas"
9
+
10
+ const TALLY_API_BASE_URL = "https://api.tally.so/"
11
+ const TALLY_API_ORIGIN = new URL(TALLY_API_BASE_URL).origin
12
+ const TALLY_API_VERSION = "2026-08-04"
13
+ const TALLY_API_KEY_SECRET_SCHEMA = z.object({
14
+ apiKey: z.string().trim().min(1),
15
+ })
16
+
17
+ /** Resolved account accepted by the shared Tally API client. */
18
+ export interface TallyResolvedAccount {
19
+ connectionMethodId: string
20
+ secret: Record<string, unknown>
21
+ serviceId: "tally"
22
+ }
23
+
24
+ /** Options for one authenticated Tally REST request. */
25
+ export interface TallyRequestOptions {
26
+ /** Tally request body. */
27
+ body?: Encodable
28
+
29
+ /** HTTP verb. Defaults to `GET`. */
30
+ method?: "DELETE" | "GET" | "PATCH" | "POST"
31
+
32
+ /** Query parameters accepted by the selected endpoint. */
33
+ query?: Record<string, number | string | string[] | null | undefined>
34
+ }
35
+
36
+ /** Options for a Tally request with a JSON response. */
37
+ export interface TallyJsonRequestOptions<TName extends TallySchemaName>
38
+ extends TallyRequestOptions {
39
+ /** Frozen OpenAPI response schema name. */
40
+ responseSchema: TName
41
+ }
42
+
43
+ /** Structured error returned by a rejected Tally request. */
44
+ export class TallyApiError extends Error {
45
+ readonly body?: Encodable
46
+ readonly retryAfter?: number
47
+ readonly status: number
48
+
49
+ /**
50
+ * Creates an error from one rejected Tally response.
51
+ *
52
+ * @param options Rejected response details.
53
+ * @param options.body Parsed provider response body.
54
+ * @param options.retryAfter Provider retry delay in seconds.
55
+ * @param options.status HTTP response status.
56
+ */
57
+ constructor(options: {
58
+ body?: Encodable
59
+ retryAfter?: number
60
+ status: number
61
+ }) {
62
+ super(
63
+ getErrorMessage(options.body) ??
64
+ `Tally API request failed with status ${options.status}.`,
65
+ )
66
+ this.name = "TallyApiError"
67
+ this.body = options.body
68
+ this.retryAfter = options.retryAfter
69
+ this.status = options.status
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Creates an authenticated Tally API client.
75
+ *
76
+ * @param account Resolved Tally API-key account.
77
+ * @throws When the account method or API key is invalid.
78
+ */
79
+ export function getTallyApi(account: TallyResolvedAccount) {
80
+ if (account.connectionMethodId !== "api-key") {
81
+ throw new Error(
82
+ `Unsupported Tally connection method: ${account.connectionMethodId}`,
83
+ )
84
+ }
85
+ const { apiKey } = TALLY_API_KEY_SECRET_SCHEMA.parse(account.secret)
86
+
87
+ return {
88
+ /**
89
+ * Sends a request and validates its JSON response.
90
+ *
91
+ * @param path Relative Tally API path.
92
+ * @param options Request and response-schema options.
93
+ */
94
+ async request<TName extends TallySchemaName>(
95
+ path: string,
96
+ options: TallyJsonRequestOptions<TName>,
97
+ ): Promise<TallySchemaValue<TName>> {
98
+ return tallySchema(options.responseSchema).parse(
99
+ await sendTallyRequest(apiKey, path, options),
100
+ )
101
+ },
102
+
103
+ /**
104
+ * Sends a request whose successful response has no body.
105
+ *
106
+ * @param path Relative Tally API path.
107
+ * @param options Request options.
108
+ */
109
+ async requestVoid(path: string, options: TallyRequestOptions) {
110
+ await sendTallyRequest(apiKey, path, options)
111
+ },
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Sends one authenticated request while enforcing the Tally API origin.
117
+ *
118
+ * @param apiKey Tally API key.
119
+ * @param path Relative Tally API path.
120
+ * @param options Request options.
121
+ */
122
+ async function sendTallyRequest(
123
+ apiKey: string,
124
+ path: string,
125
+ options: TallyRequestOptions,
126
+ ) {
127
+ const normalizedPath = path.replace(/^\/+/, "")
128
+ if (
129
+ !normalizedPath ||
130
+ normalizedPath.includes("://") ||
131
+ normalizedPath.includes("\\") ||
132
+ normalizedPath.includes("?") ||
133
+ normalizedPath.includes("#") ||
134
+ normalizedPath.split("/").some(isTraversalSegment)
135
+ ) {
136
+ throw new TypeError("Tally API paths must be relative.")
137
+ }
138
+ const url = new URL(normalizedPath, TALLY_API_BASE_URL)
139
+ if (url.origin !== TALLY_API_ORIGIN) {
140
+ throw new TypeError("Tally API paths must remain on api.tally.so.")
141
+ }
142
+ for (const [name, value] of Object.entries(options.query ?? {})) {
143
+ if (value == null) continue
144
+ if (Array.isArray(value)) {
145
+ for (const item of value) url.searchParams.append(name, item)
146
+ } else {
147
+ url.searchParams.set(name, String(value))
148
+ }
149
+ }
150
+
151
+ const response = await fetch(url, {
152
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
153
+ headers: {
154
+ Accept: "application/json",
155
+ Authorization: `Bearer ${apiKey}`,
156
+ "tally-version": TALLY_API_VERSION,
157
+ ...(options.body === undefined
158
+ ? {}
159
+ : { "Content-Type": "application/json" }),
160
+ },
161
+ method: options.method ?? "GET",
162
+ redirect: "error",
163
+ })
164
+ const text = await response.text()
165
+ const parsed = text ? parseJson(text) : undefined
166
+ if (!response.ok) {
167
+ const body = encodableSchema.safeParse(parsed)
168
+ throw new TallyApiError({
169
+ ...(body.success && { body: body.data }),
170
+ retryAfter: parseRetryAfter(response.headers.get("Retry-After")),
171
+ status: response.status,
172
+ })
173
+ }
174
+ return parsed
175
+ }
176
+
177
+ /**
178
+ * Detects direct or encoded path traversal segments.
179
+ *
180
+ * @param segment One URL path segment.
181
+ */
182
+ function isTraversalSegment(segment: string) {
183
+ try {
184
+ const decoded = decodeURIComponent(segment)
185
+ return decoded === "." || decoded === ".." || decoded.includes("/")
186
+ } catch {
187
+ return true
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Parses one JSON response or reports a provider contract failure.
193
+ *
194
+ * @param text Provider response text.
195
+ * @throws When the provider response is not valid JSON.
196
+ */
197
+ function parseJson(text: string) {
198
+ try {
199
+ return JSON.parse(text) as unknown
200
+ } catch {
201
+ throw new Error("Tally API returned invalid JSON.")
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Parses Retry-After seconds or an HTTP date into a delay in seconds.
207
+ *
208
+ * @param value Retry-After header value.
209
+ */
210
+ function parseRetryAfter(value: string | null) {
211
+ if (!value) return undefined
212
+ const seconds = Number(value)
213
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds
214
+ const date = Date.parse(value)
215
+ return Number.isNaN(date)
216
+ ? undefined
217
+ : Math.max(0, (date - Date.now()) / 1_000)
218
+ }
219
+
220
+ /**
221
+ * Extracts the provider's human-readable error message.
222
+ *
223
+ * @param body Parsed provider error response.
224
+ */
225
+ function getErrorMessage(body: Encodable | undefined) {
226
+ if (typeof body === "string") return body
227
+ if (!body || Array.isArray(body) || typeof body !== "object") return undefined
228
+ for (const key of ["message", "error"]) {
229
+ const value = Reflect.get(body, key)
230
+ if (typeof value === "string" && value) return value
231
+ }
232
+ return undefined
233
+ }
@@ -0,0 +1,88 @@
1
+ import * as z from "zod"
2
+
3
+ export const TALLY_FORM_RESPONSE_EVENT_TYPE = "FORM_RESPONSE" as const
4
+
5
+ const TALLY_OPTION_SCHEMA = z.object({ id: z.string(), text: z.string() })
6
+ const TALLY_FILE_SCHEMA = z.object({
7
+ id: z.string(),
8
+ mimeType: z.string(),
9
+ name: z.string(),
10
+ size: z.number().int().nonnegative(),
11
+ url: z.url(),
12
+ })
13
+ const TALLY_FIELD_SCHEMA = z.object({
14
+ columns: TALLY_OPTION_SCHEMA.array().optional(),
15
+ key: z.string(),
16
+ label: z.string(),
17
+ options: TALLY_OPTION_SCHEMA.array().optional(),
18
+ rows: TALLY_OPTION_SCHEMA.array().optional(),
19
+ type: z.enum([
20
+ "CALCULATED_FIELDS",
21
+ "CHECKBOXES",
22
+ "DROPDOWN",
23
+ "FILE_UPLOAD",
24
+ "HIDDEN_FIELDS",
25
+ "INPUT_DATE",
26
+ "INPUT_EMAIL",
27
+ "INPUT_LINK",
28
+ "INPUT_NUMBER",
29
+ "INPUT_PHONE_NUMBER",
30
+ "INPUT_TEXT",
31
+ "INPUT_TIME",
32
+ "LINEAR_SCALE",
33
+ "MATRIX",
34
+ "MULTIPLE_CHOICE",
35
+ "MULTI_SELECT",
36
+ "PAYMENT",
37
+ "RANKING",
38
+ "RATING",
39
+ "SIGNATURE",
40
+ "TEXTAREA",
41
+ ]),
42
+ value: z.union([
43
+ z.string(),
44
+ z.number(),
45
+ z.boolean(),
46
+ z.string().array(),
47
+ TALLY_FILE_SCHEMA.array(),
48
+ z.record(z.string(), z.string().array()),
49
+ z.null(),
50
+ ]),
51
+ })
52
+
53
+ /** Signed form-response webhook payload delivered by Tally. */
54
+ export const TALLY_FORM_RESPONSE_EVENT_SCHEMA = z.object({
55
+ createdAt: z.iso.datetime({ offset: true }),
56
+ data: z.object({
57
+ createdAt: z.iso.datetime({ offset: true }),
58
+ fields: TALLY_FIELD_SCHEMA.array(),
59
+ formId: z.string(),
60
+ formName: z.string(),
61
+ respondentId: z.string(),
62
+ responseId: z.string(),
63
+ submissionId: z.string(),
64
+ submissionPdfUrl: z.url().optional(),
65
+ submissionPreviewUrl: z.url().optional(),
66
+ }),
67
+ eventId: z.string(),
68
+ eventType: z.literal(TALLY_FORM_RESPONSE_EVENT_TYPE),
69
+ })
70
+
71
+ export type TallyFormResponseEvent = z.output<
72
+ typeof TALLY_FORM_RESPONSE_EVENT_SCHEMA
73
+ >
74
+
75
+ export const TALLY_TRIGGER_CONFIG_SCHEMA = z.object({
76
+ formId: z.string().trim().min(1),
77
+ })
78
+
79
+ export const tallyTriggerContracts = {
80
+ "tally.form.event": {
81
+ configSchema: TALLY_TRIGGER_CONFIG_SCHEMA,
82
+ eventSchema: TALLY_FORM_RESPONSE_EVENT_SCHEMA,
83
+ },
84
+ "tally.form.response": {
85
+ configSchema: TALLY_TRIGGER_CONFIG_SCHEMA,
86
+ eventSchema: TALLY_FORM_RESPONSE_EVENT_SCHEMA,
87
+ },
88
+ } as const
@@ -0,0 +1,3 @@
1
+ export * from "./api"
2
+ export * from "./events"
3
+ export * from "./schemas"