@keystrokehq/typeform 0.0.1
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.
- package/README.md +159 -0
- package/dist/_official/index.d.mts +3 -0
- package/dist/_official/index.mjs +3 -0
- package/dist/_runtime/index.d.mts +1 -0
- package/dist/_runtime/index.mjs +1 -0
- package/dist/accounts.d.mts +17 -0
- package/dist/accounts.mjs +20 -0
- package/dist/client.d.mts +104 -0
- package/dist/client.mjs +308 -0
- package/dist/connection.d.mts +2 -0
- package/dist/connection.mjs +3 -0
- package/dist/events.d.mts +160 -0
- package/dist/events.mjs +38 -0
- package/dist/factory-B_dyn39A.mjs +8 -0
- package/dist/forms.d.mts +390 -0
- package/dist/forms.mjs +147 -0
- package/dist/images.d.mts +112 -0
- package/dist/images.mjs +104 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/insights.d.mts +26 -0
- package/dist/insights.mjs +37 -0
- package/dist/integration-BCzgn7Dm.mjs +76 -0
- package/dist/integration-d_VqlGvZ.d.mts +38 -0
- package/dist/messaging.d.mts +1 -0
- package/dist/messaging.mjs +1 -0
- package/dist/provider-app-BnLMYj3B.d.mts +35 -0
- package/dist/responses.d.mts +118 -0
- package/dist/responses.mjs +107 -0
- package/dist/schemas/index.d.mts +564 -0
- package/dist/schemas/index.mjs +3 -0
- package/dist/shared-D0ONnQL8.mjs +49 -0
- package/dist/themes.d.mts +225 -0
- package/dist/themes.mjs +132 -0
- package/dist/triggers.d.mts +21 -0
- package/dist/triggers.mjs +45 -0
- package/dist/verification.d.mts +19 -0
- package/dist/verification.mjs +33 -0
- package/dist/videos.d.mts +20 -0
- package/dist/videos.mjs +29 -0
- package/dist/webhooks.d.mts +73 -0
- package/dist/webhooks.mjs +79 -0
- package/dist/workspace-C1CCnvgv.mjs +271 -0
- package/dist/workspaces.d.mts +145 -0
- package/dist/workspaces.mjs +119 -0
- package/package.json +131 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @keystrokehq/typeform
|
|
2
|
+
|
|
3
|
+
Official Keystroke integration for [Typeform](https://www.typeform.com/).
|
|
4
|
+
|
|
5
|
+
Ship code-first automations over Typeform forms, responses, themes, images, workspaces, webhooks, and account metadata, plus a direct-binding webhook trigger that fires when a respondent submits a form.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @keystrokehq/typeform
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Connect
|
|
14
|
+
|
|
15
|
+
Typeform supports two authentication modes. Both are handled by the same public connection credential set — a user connecting an account through Keystroke's OAuth flow and a server-to-server install using a Personal Access Token (PAT) share the same runtime contract.
|
|
16
|
+
|
|
17
|
+
### OAuth (recommended — "click to connect")
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
keystroke connect typeform
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This triggers the standard Keystroke OAuth 2.0 flow against `https://api.typeform.com/oauth/authorize`, requests the full read/write scopes plus `offline` (for refresh tokens), and stores the resulting token under the `TYPEFORM_ACCESS_TOKEN` vault key.
|
|
24
|
+
|
|
25
|
+
### Personal Access Token (server-to-server)
|
|
26
|
+
|
|
27
|
+
Generate a PAT in your Typeform **Personal Tokens** settings and populate the vault entry directly:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export TYPEFORM_ACCESS_TOKEN=tfp_xxxxxxxx…
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Typeform treats OAuth access tokens and PATs identically at the wire level (`Authorization: Bearer <token>`), so all operations and triggers in this package work against either.
|
|
34
|
+
|
|
35
|
+
## Public surface
|
|
36
|
+
|
|
37
|
+
Authored workflow code should import **only** these subpaths:
|
|
38
|
+
|
|
39
|
+
| Subpath | What it exports |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `@keystrokehq/typeform/connection` | `typeform` credential set + `TypeformCredentials` type |
|
|
42
|
+
| `@keystrokehq/typeform/client` | `createTypeformClient`, `TypeformApiError`, `createLeakyBucketRateLimiter` |
|
|
43
|
+
| `@keystrokehq/typeform/schemas` | Reusable Zod schemas (`typeformFormSchema`, `typeformResponseSchema`, …) |
|
|
44
|
+
| `@keystrokehq/typeform/events` | Curated trigger event schemas + types |
|
|
45
|
+
| `@keystrokehq/typeform/verification` | `verifyTypeformWebhookRequest`, `buildTypeformWebhookSignature` |
|
|
46
|
+
| `@keystrokehq/typeform/triggers` | `webhooks.formResponseSubmitted(...)` direct binding |
|
|
47
|
+
| `@keystrokehq/typeform/accounts` | `getAboutMe` |
|
|
48
|
+
| `@keystrokehq/typeform/forms` | `listForms`, `getForm`, `createForm`, `updateForm`, `patchForm`, `deleteForm` |
|
|
49
|
+
| `@keystrokehq/typeform/responses` | `getFormResponses`, `deleteResponses`, `getAllResponseFiles` |
|
|
50
|
+
| `@keystrokehq/typeform/themes` | `listThemes`, `getTheme`, `createTheme`, `updateTheme`, `patchTheme`, `deleteTheme` |
|
|
51
|
+
| `@keystrokehq/typeform/images` | `listImages`, `createImage`, `getImageBySize`, `getBackgroundBySize`, `getChoiceImageBySize`, `deleteImage` |
|
|
52
|
+
| `@keystrokehq/typeform/videos` | `uploadVideo` |
|
|
53
|
+
| `@keystrokehq/typeform/workspaces` | `listWorkspaces`, `getWorkspace`, `createWorkspace`, `createAccountWorkspace`, `updateWorkspace`, `deleteWorkspace` |
|
|
54
|
+
| `@keystrokehq/typeform/webhooks` | `listWebhooks`, `getWebhook`, `createOrUpdateWebhook`, `deleteWebhook` |
|
|
55
|
+
| `@keystrokehq/typeform/insights` | `getFormMessages`, `updateFormMessages` |
|
|
56
|
+
|
|
57
|
+
Hidden, repo-only subpaths (`_official`, `_runtime`) are for platform internals — **do not import these from authored code**.
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
### Call a Typeform operation inside a workflow
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { Workflow } from '@keystrokehq/core';
|
|
65
|
+
import { listForms } from '@keystrokehq/typeform/forms';
|
|
66
|
+
import { z } from 'zod';
|
|
67
|
+
|
|
68
|
+
export const triageNewForms = new Workflow({
|
|
69
|
+
id: 'triage_new_forms',
|
|
70
|
+
name: 'Triage new Typeform forms',
|
|
71
|
+
input: z.object({}),
|
|
72
|
+
output: z.object({ count: z.number() }),
|
|
73
|
+
async run() {
|
|
74
|
+
const { items } = await listForms.run({ pageSize: 20 });
|
|
75
|
+
return { count: items.length };
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### React to form submissions (direct-binding webhook trigger)
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { Workflow } from '@keystrokehq/core';
|
|
84
|
+
import { webhooks } from '@keystrokehq/typeform/triggers';
|
|
85
|
+
import { z } from 'zod';
|
|
86
|
+
|
|
87
|
+
export const captureLead = new Workflow({
|
|
88
|
+
id: 'capture_typeform_lead',
|
|
89
|
+
name: 'Capture Lead',
|
|
90
|
+
triggers: [
|
|
91
|
+
webhooks.formResponseSubmitted({
|
|
92
|
+
filter: (event) => event.formId === 'LEADS2026',
|
|
93
|
+
transform: (event) => ({
|
|
94
|
+
formId: event.formId,
|
|
95
|
+
token: event.token,
|
|
96
|
+
email: event.answers?.find((a) => a.field.type === 'email')?.email,
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
],
|
|
100
|
+
input: z.object({ formId: z.string(), token: z.string(), email: z.string().optional() }),
|
|
101
|
+
output: z.object({ ok: z.boolean() }),
|
|
102
|
+
async run() {
|
|
103
|
+
return { ok: true };
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Support surfaces
|
|
109
|
+
|
|
110
|
+
### Using the raw Typeform client
|
|
111
|
+
|
|
112
|
+
Most workflows should call operations directly. When you need more control (e.g. custom pagination), drop to the client:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { createTypeformClient } from '@keystrokehq/typeform/client';
|
|
116
|
+
import type { TypeformCredentials } from '@keystrokehq/typeform/connection';
|
|
117
|
+
|
|
118
|
+
export async function listAllForms(credentials: TypeformCredentials) {
|
|
119
|
+
const client = createTypeformClient(credentials);
|
|
120
|
+
return client.forms.list({ page_size: 200 });
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Signature verification
|
|
125
|
+
|
|
126
|
+
Use `verifyTypeformWebhookRequest` when you accept Typeform webhooks on a custom route (the built-in trigger already verifies for you):
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { verifyTypeformWebhookRequest } from '@keystrokehq/typeform/verification';
|
|
130
|
+
|
|
131
|
+
const ok = verifyTypeformWebhookRequest(
|
|
132
|
+
{ headers: request.headers, rawBody: request.rawBody },
|
|
133
|
+
webhookSigningSecret
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Provisioning a webhook
|
|
138
|
+
|
|
139
|
+
`createOrUpdateWebhook` wires the integration's shared signing secret onto a new or updated webhook subscription (unsigned webhooks are rejected at the schema boundary):
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { createOrUpdateWebhook } from '@keystrokehq/typeform/webhooks';
|
|
143
|
+
|
|
144
|
+
await createOrUpdateWebhook.run({
|
|
145
|
+
formId: 'LEADS2026',
|
|
146
|
+
tag: 'keystroke-capture-lead',
|
|
147
|
+
url: 'https://webhooks.keystroke.dev/typeform',
|
|
148
|
+
secret: process.env.TYPEFORM_WEBHOOK_SECRET!,
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Caveats
|
|
153
|
+
|
|
154
|
+
- **Single native webhook event.** Typeform only emits `form_response` when a respondent completes a submission. There are no partial-response or form-mutation webhooks; use polling (`getFormResponses` with a cursor) if you need finer granularity.
|
|
155
|
+
- **Signed file URLs expire quickly.** `getAllResponseFiles` returns the signed URL path; fetch the content immediately and, if you need durable storage, re-host it yourself.
|
|
156
|
+
- **PAT vs OAuth parity.** OAuth access tokens refresh automatically through the Keystroke vault. PATs do not; rotate them on the Typeform side and update the vault entry.
|
|
157
|
+
- **Rate limits.** Typeform publishes a baseline of 2 requests/second per token. The client enforces a leaky-bucket limiter by default and retries `429` with `Retry-After`.
|
|
158
|
+
- **Irreversible response deletion.** `deleteResponses` requires an explicit `confirm: true` flag in input to prevent accidental data loss.
|
|
159
|
+
- **Webhook signing is mandatory.** `createOrUpdateWebhook` refuses to provision an unsigned webhook; the trigger likewise fails closed when the internal app credential set has no `webhookSecret`.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { i as typeformOfficialIntegration, r as typeformBundle } from "../integration-d_VqlGvZ.mjs";
|
|
2
|
+
import { n as typeformAppCredentialSet, r as typeformPlatformProviderSeed, t as TypeformAppCredentials } from "../provider-app-BnLMYj3B.mjs";
|
|
3
|
+
export { TypeformAppCredentials, typeformAppCredentialSet, typeformBundle, typeformOfficialIntegration, typeformPlatformProviderSeed };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as typeformPlatformProviderSeed, i as typeformAppCredentialSet, n as typeformBundle, r as typeformOfficialIntegration } from "../integration-BCzgn7Dm.mjs";
|
|
2
|
+
|
|
3
|
+
export { typeformAppCredentialSet, typeformBundle, typeformOfficialIntegration, typeformPlatformProviderSeed };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as _keystrokehq_core0 from "@keystrokehq/core";
|
|
3
|
+
import * as _keystrokehq_core_credential_set0 from "@keystrokehq/core/credential-set";
|
|
4
|
+
|
|
5
|
+
//#region src/accounts.d.ts
|
|
6
|
+
declare const getAboutMe: _keystrokehq_core0.Operation<z.ZodObject<{}, z.core.$strip>, z.ZodObject<{
|
|
7
|
+
alias: z.ZodOptional<z.ZodString>;
|
|
8
|
+
email: z.ZodOptional<z.ZodString>;
|
|
9
|
+
language: z.ZodOptional<z.ZodString>;
|
|
10
|
+
user_id: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$catchall<z.ZodUnknown>>, readonly [_keystrokehq_core0.CredentialSet<"typeform", z.ZodObject<{
|
|
12
|
+
TYPEFORM_ACCESS_TOKEN: z.ZodString;
|
|
13
|
+
}, z.core.$strip>, readonly _keystrokehq_core_credential_set0.CredentialConnection<z.ZodObject<{
|
|
14
|
+
TYPEFORM_ACCESS_TOKEN: z.ZodString;
|
|
15
|
+
}, z.core.$strip>>[] | undefined>], undefined>;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { getAboutMe };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createTypeformClient } from "./client.mjs";
|
|
2
|
+
import { w as typeformAccountSchema } from "./workspace-C1CCnvgv.mjs";
|
|
3
|
+
import { t as typeformOperation } from "./factory-B_dyn39A.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/accounts.ts
|
|
7
|
+
const getAboutMe = typeformOperation({
|
|
8
|
+
id: "get_about_me",
|
|
9
|
+
name: "Get About Me",
|
|
10
|
+
description: "Retrieve basic information about the connected Typeform account.",
|
|
11
|
+
input: z.object({}),
|
|
12
|
+
output: typeformAccountSchema,
|
|
13
|
+
run: async (_input, credentials) => {
|
|
14
|
+
const response = await createTypeformClient(credentials).accounts.getAboutMe();
|
|
15
|
+
return typeformAccountSchema.parse(response);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { getAboutMe };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { t as TypeformCredentials } from "./integration-d_VqlGvZ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/client.d.ts
|
|
4
|
+
type Primitive = string | number | boolean;
|
|
5
|
+
type QueryValue = Primitive | readonly Primitive[] | undefined;
|
|
6
|
+
type TypeformApiErrorKind = 'auth' | 'http' | 'rate_limit' | 'validation';
|
|
7
|
+
interface TypeformApiErrorOptions {
|
|
8
|
+
readonly kind: TypeformApiErrorKind;
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly body: unknown;
|
|
11
|
+
readonly requestId?: string;
|
|
12
|
+
readonly providerCode?: string;
|
|
13
|
+
readonly retryAfterSeconds?: number;
|
|
14
|
+
readonly retryable: boolean;
|
|
15
|
+
}
|
|
16
|
+
declare class TypeformApiError extends Error {
|
|
17
|
+
readonly kind: TypeformApiErrorKind;
|
|
18
|
+
readonly status: number;
|
|
19
|
+
readonly body: unknown;
|
|
20
|
+
readonly requestId: string | undefined;
|
|
21
|
+
readonly providerCode: string | undefined;
|
|
22
|
+
readonly retryAfterSeconds: number | undefined;
|
|
23
|
+
readonly retryable: boolean;
|
|
24
|
+
constructor(message: string, options: TypeformApiErrorOptions);
|
|
25
|
+
}
|
|
26
|
+
interface TypeformRateLimiter {
|
|
27
|
+
readonly acquire: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
interface LeakyBucketOptions {
|
|
30
|
+
readonly ratePerSecond?: number;
|
|
31
|
+
readonly burst?: number;
|
|
32
|
+
}
|
|
33
|
+
declare function createLeakyBucketRateLimiter(options?: LeakyBucketOptions): TypeformRateLimiter;
|
|
34
|
+
interface TypeformClientConfig {
|
|
35
|
+
readonly baseUrl?: string;
|
|
36
|
+
readonly rateLimiter?: TypeformRateLimiter;
|
|
37
|
+
readonly fetch?: typeof fetch;
|
|
38
|
+
readonly maxRateLimitRetries?: number;
|
|
39
|
+
}
|
|
40
|
+
interface TypeformListResponse<T> {
|
|
41
|
+
readonly items: T[];
|
|
42
|
+
readonly pageCount?: number;
|
|
43
|
+
readonly totalItems?: number;
|
|
44
|
+
readonly pageSize?: number;
|
|
45
|
+
}
|
|
46
|
+
declare function createTypeformClient(credentials: TypeformCredentials, config?: TypeformClientConfig): {
|
|
47
|
+
accounts: {
|
|
48
|
+
getAboutMe: () => Promise<unknown>;
|
|
49
|
+
};
|
|
50
|
+
forms: {
|
|
51
|
+
list: (query?: Record<string, QueryValue>) => Promise<unknown>;
|
|
52
|
+
get: (formId: string) => Promise<unknown>;
|
|
53
|
+
create: (body: unknown) => Promise<unknown>;
|
|
54
|
+
update: (formId: string, body: unknown) => Promise<unknown>;
|
|
55
|
+
patch: (formId: string, body: unknown) => Promise<unknown>;
|
|
56
|
+
delete: (formId: string) => Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
responses: {
|
|
59
|
+
list: (formId: string, query?: Record<string, QueryValue>) => Promise<unknown>;
|
|
60
|
+
delete: (formId: string, includedTokens: readonly string[]) => Promise<void>;
|
|
61
|
+
};
|
|
62
|
+
responseFiles: {
|
|
63
|
+
download: (formId: string, responseId: string, fieldId: string, filename: string) => Promise<unknown>;
|
|
64
|
+
};
|
|
65
|
+
themes: {
|
|
66
|
+
list: (query?: Record<string, QueryValue>) => Promise<unknown>;
|
|
67
|
+
get: (themeId: string) => Promise<unknown>;
|
|
68
|
+
create: (body: unknown) => Promise<unknown>;
|
|
69
|
+
update: (themeId: string, body: unknown) => Promise<unknown>;
|
|
70
|
+
patch: (themeId: string, body: unknown) => Promise<unknown>;
|
|
71
|
+
delete: (themeId: string) => Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
images: {
|
|
74
|
+
list: () => Promise<unknown>;
|
|
75
|
+
create: (body: unknown) => Promise<unknown>;
|
|
76
|
+
getBySize: (imageId: string, size: string) => Promise<unknown>;
|
|
77
|
+
getBackgroundBySize: (imageId: string, size: string) => Promise<unknown>;
|
|
78
|
+
getChoiceBySize: (imageId: string, size: string) => Promise<unknown>;
|
|
79
|
+
delete: (imageId: string) => Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
videos: {
|
|
82
|
+
upload: (body: unknown) => Promise<unknown>;
|
|
83
|
+
};
|
|
84
|
+
workspaces: {
|
|
85
|
+
list: (query?: Record<string, QueryValue>) => Promise<unknown>;
|
|
86
|
+
get: (workspaceId: string) => Promise<unknown>;
|
|
87
|
+
create: (body: unknown) => Promise<unknown>;
|
|
88
|
+
update: (workspaceId: string, body: unknown) => Promise<unknown>;
|
|
89
|
+
delete: (workspaceId: string) => Promise<void>;
|
|
90
|
+
};
|
|
91
|
+
webhooks: {
|
|
92
|
+
list: (formId: string) => Promise<unknown>;
|
|
93
|
+
get: (formId: string, tag: string) => Promise<unknown>;
|
|
94
|
+
createOrUpdate: (formId: string, tag: string, body: unknown) => Promise<unknown>;
|
|
95
|
+
delete: (formId: string, tag: string) => Promise<void>;
|
|
96
|
+
};
|
|
97
|
+
insights: {
|
|
98
|
+
getFormMessages: (formId: string) => Promise<unknown>;
|
|
99
|
+
updateFormMessages: (formId: string, body: unknown) => Promise<void>;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
type TypeformClient = ReturnType<typeof createTypeformClient>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export { TypeformApiError, TypeformApiErrorKind, TypeformApiErrorOptions, TypeformClient, TypeformClientConfig, type TypeformListResponse, TypeformRateLimiter, createLeakyBucketRateLimiter, createTypeformClient };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { t as typeform } from "./integration-BCzgn7Dm.mjs";
|
|
2
|
+
import { n as TYPEFORM_CONNECT_HINT, t as TYPEFORM_API_BASE_URL } from "./shared-D0ONnQL8.mjs";
|
|
3
|
+
import { CredentialRevokedError } from "@keystrokehq/core/errors";
|
|
4
|
+
import { createErrorNormalizingProxy } from "@keystrokehq/integration-authoring";
|
|
5
|
+
|
|
6
|
+
//#region src/client.ts
|
|
7
|
+
var TypeformApiError = class extends Error {
|
|
8
|
+
kind;
|
|
9
|
+
status;
|
|
10
|
+
body;
|
|
11
|
+
requestId;
|
|
12
|
+
providerCode;
|
|
13
|
+
retryAfterSeconds;
|
|
14
|
+
retryable;
|
|
15
|
+
constructor(message, options) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "TypeformApiError";
|
|
18
|
+
this.kind = options.kind;
|
|
19
|
+
this.status = options.status;
|
|
20
|
+
this.body = options.body;
|
|
21
|
+
this.requestId = options.requestId;
|
|
22
|
+
this.providerCode = options.providerCode;
|
|
23
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
24
|
+
this.retryable = options.retryable;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === "object" && value !== null;
|
|
29
|
+
}
|
|
30
|
+
function extractErrorMessage(body, status) {
|
|
31
|
+
if (isRecord(body)) {
|
|
32
|
+
const description = typeof body.description === "string" ? body.description : void 0;
|
|
33
|
+
const message = typeof body.message === "string" ? body.message : void 0;
|
|
34
|
+
return description ?? message ?? `HTTP ${status}`;
|
|
35
|
+
}
|
|
36
|
+
return `HTTP ${status}`;
|
|
37
|
+
}
|
|
38
|
+
function extractProviderCode(body) {
|
|
39
|
+
if (isRecord(body) && typeof body.code === "string") return body.code;
|
|
40
|
+
}
|
|
41
|
+
function classifyError(status, _body, retryAfterSeconds) {
|
|
42
|
+
if (status === 429) return {
|
|
43
|
+
kind: "rate_limit",
|
|
44
|
+
retryable: true
|
|
45
|
+
};
|
|
46
|
+
if (status === 401 || status === 403) return {
|
|
47
|
+
kind: "auth",
|
|
48
|
+
retryable: false
|
|
49
|
+
};
|
|
50
|
+
if (status === 422) return {
|
|
51
|
+
kind: "validation",
|
|
52
|
+
retryable: false
|
|
53
|
+
};
|
|
54
|
+
if (status >= 500 && status < 600) return {
|
|
55
|
+
kind: "http",
|
|
56
|
+
retryable: true
|
|
57
|
+
};
|
|
58
|
+
if (retryAfterSeconds !== void 0) return {
|
|
59
|
+
kind: "http",
|
|
60
|
+
retryable: true
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
kind: "http",
|
|
64
|
+
retryable: false
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseRetryAfter(value) {
|
|
68
|
+
if (!value) return void 0;
|
|
69
|
+
const asNumber = Number(value);
|
|
70
|
+
if (Number.isFinite(asNumber) && asNumber >= 0) return asNumber;
|
|
71
|
+
const asDate = Date.parse(value);
|
|
72
|
+
if (!Number.isNaN(asDate)) return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
|
|
73
|
+
}
|
|
74
|
+
function isAuthError(error) {
|
|
75
|
+
return error instanceof TypeformApiError && error.kind === "auth";
|
|
76
|
+
}
|
|
77
|
+
function normalizeTypeformError(error, context) {
|
|
78
|
+
if (isAuthError(error)) return new CredentialRevokedError(typeform.id, `Typeform authentication failed while calling \`${context.methodPath}\`. The stored credentials for ${typeform.id} may be revoked or expired. ${TYPEFORM_CONNECT_HINT}`);
|
|
79
|
+
if (error instanceof TypeformApiError) return new Error(`Typeform API call failed while calling \`${context.methodPath}\` (${error.status}): ${error.message}`, { cause: error });
|
|
80
|
+
if (error instanceof Error) return new Error(`Typeform API call failed while calling \`${context.methodPath}\`: ${error.message}`, { cause: error });
|
|
81
|
+
return /* @__PURE__ */ new Error(`Typeform API call failed while calling \`${context.methodPath}\`.`);
|
|
82
|
+
}
|
|
83
|
+
function appendQuery(searchParams, query) {
|
|
84
|
+
if (!query) return;
|
|
85
|
+
for (const [key, value] of Object.entries(query)) {
|
|
86
|
+
if (value === void 0) continue;
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
if (value.length === 0) continue;
|
|
89
|
+
searchParams.set(key, value.map(String).join(","));
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
searchParams.set(key, String(value));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function createLeakyBucketRateLimiter(options = {}) {
|
|
96
|
+
const ratePerSecond = options.ratePerSecond ?? 2;
|
|
97
|
+
const burst = options.burst ?? 2;
|
|
98
|
+
const intervalMs = 1e3 / ratePerSecond;
|
|
99
|
+
let tokens = burst;
|
|
100
|
+
let lastRefill = Date.now();
|
|
101
|
+
let chain = Promise.resolve();
|
|
102
|
+
async function acquireOne() {
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const elapsed = now - lastRefill;
|
|
105
|
+
if (elapsed > 0) {
|
|
106
|
+
const refill = Math.floor(elapsed / intervalMs);
|
|
107
|
+
if (refill > 0) {
|
|
108
|
+
tokens = Math.min(burst, tokens + refill);
|
|
109
|
+
lastRefill = lastRefill + refill * intervalMs;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (tokens > 0) {
|
|
113
|
+
tokens -= 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const waitMs = Math.max(0, lastRefill + intervalMs - now);
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
118
|
+
tokens = Math.min(burst, tokens + 1);
|
|
119
|
+
lastRefill = Date.now();
|
|
120
|
+
tokens -= 1;
|
|
121
|
+
}
|
|
122
|
+
return { acquire: () => {
|
|
123
|
+
const next = chain.then(acquireOne);
|
|
124
|
+
chain = next.catch(() => void 0);
|
|
125
|
+
return next;
|
|
126
|
+
} };
|
|
127
|
+
}
|
|
128
|
+
function makeRequest(accessToken, config) {
|
|
129
|
+
const baseUrl = config.baseUrl ?? TYPEFORM_API_BASE_URL;
|
|
130
|
+
const limiter = config.rateLimiter ?? createLeakyBucketRateLimiter();
|
|
131
|
+
const fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
132
|
+
const maxRateLimitRetries = config.maxRateLimitRetries ?? 3;
|
|
133
|
+
async function request(path, options = {}) {
|
|
134
|
+
let attempt = 0;
|
|
135
|
+
for (;;) {
|
|
136
|
+
await limiter.acquire();
|
|
137
|
+
const url = new URL(`${baseUrl}${path}`);
|
|
138
|
+
const query = new URLSearchParams();
|
|
139
|
+
appendQuery(query, options.query);
|
|
140
|
+
const search = query.toString();
|
|
141
|
+
if (search.length > 0) url.search = search;
|
|
142
|
+
const headers = {
|
|
143
|
+
Authorization: `Bearer ${accessToken}`,
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
...options.headers ?? {}
|
|
146
|
+
};
|
|
147
|
+
const response = await fetchImpl(url, {
|
|
148
|
+
method: options.method ?? "GET",
|
|
149
|
+
headers,
|
|
150
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
151
|
+
});
|
|
152
|
+
if (response.status === 204) return;
|
|
153
|
+
const responseBody = (response.headers.get("content-type") ?? "").includes("application/json") ? await response.json() : await response.text();
|
|
154
|
+
if (response.ok) return responseBody;
|
|
155
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
156
|
+
const { kind, retryable } = classifyError(response.status, responseBody, retryAfter);
|
|
157
|
+
if (kind === "rate_limit" && attempt < maxRateLimitRetries) {
|
|
158
|
+
attempt += 1;
|
|
159
|
+
const waitMs = Math.max(0, (retryAfter ?? 1) * 1e3);
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
164
|
+
throw new TypeformApiError(extractErrorMessage(responseBody, response.status), {
|
|
165
|
+
kind,
|
|
166
|
+
status: response.status,
|
|
167
|
+
body: responseBody,
|
|
168
|
+
providerCode: extractProviderCode(responseBody),
|
|
169
|
+
retryAfterSeconds: retryAfter,
|
|
170
|
+
retryable,
|
|
171
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return request;
|
|
176
|
+
}
|
|
177
|
+
function accountsApi(request) {
|
|
178
|
+
return { getAboutMe: () => request("/me") };
|
|
179
|
+
}
|
|
180
|
+
function formsApi(request) {
|
|
181
|
+
return {
|
|
182
|
+
list: (query) => request("/forms", { query }),
|
|
183
|
+
get: (formId) => request(`/forms/${encodeURIComponent(formId)}`),
|
|
184
|
+
create: (body) => request("/forms", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body
|
|
187
|
+
}),
|
|
188
|
+
update: (formId, body) => request(`/forms/${encodeURIComponent(formId)}`, {
|
|
189
|
+
method: "PUT",
|
|
190
|
+
body
|
|
191
|
+
}),
|
|
192
|
+
patch: (formId, body) => request(`/forms/${encodeURIComponent(formId)}`, {
|
|
193
|
+
method: "PATCH",
|
|
194
|
+
body
|
|
195
|
+
}),
|
|
196
|
+
delete: (formId) => request(`/forms/${encodeURIComponent(formId)}`, { method: "DELETE" })
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function responsesApi(request) {
|
|
200
|
+
return {
|
|
201
|
+
list: (formId, query) => request(`/forms/${encodeURIComponent(formId)}/responses`, { query }),
|
|
202
|
+
delete: (formId, includedTokens) => request(`/forms/${encodeURIComponent(formId)}/responses`, {
|
|
203
|
+
method: "DELETE",
|
|
204
|
+
query: { included_tokens: includedTokens }
|
|
205
|
+
})
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function responseFilesApi(request) {
|
|
209
|
+
return { download: async (formId, responseId, fieldId, filename) => {
|
|
210
|
+
return request(`/forms/${encodeURIComponent(formId)}/responses/${encodeURIComponent(responseId)}/fields/${encodeURIComponent(fieldId)}/files/${encodeURIComponent(filename)}`, { rawResponse: true });
|
|
211
|
+
} };
|
|
212
|
+
}
|
|
213
|
+
function themesApi(request) {
|
|
214
|
+
return {
|
|
215
|
+
list: (query) => request("/themes", { query }),
|
|
216
|
+
get: (themeId) => request(`/themes/${encodeURIComponent(themeId)}`),
|
|
217
|
+
create: (body) => request("/themes", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
body
|
|
220
|
+
}),
|
|
221
|
+
update: (themeId, body) => request(`/themes/${encodeURIComponent(themeId)}`, {
|
|
222
|
+
method: "PUT",
|
|
223
|
+
body
|
|
224
|
+
}),
|
|
225
|
+
patch: (themeId, body) => request(`/themes/${encodeURIComponent(themeId)}`, {
|
|
226
|
+
method: "PATCH",
|
|
227
|
+
body
|
|
228
|
+
}),
|
|
229
|
+
delete: (themeId) => request(`/themes/${encodeURIComponent(themeId)}`, { method: "DELETE" })
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function imagesApi(request) {
|
|
233
|
+
return {
|
|
234
|
+
list: () => request("/images"),
|
|
235
|
+
create: (body) => request("/images", {
|
|
236
|
+
method: "POST",
|
|
237
|
+
body
|
|
238
|
+
}),
|
|
239
|
+
getBySize: (imageId, size) => request(`/images/${encodeURIComponent(imageId)}/image/${encodeURIComponent(size)}`),
|
|
240
|
+
getBackgroundBySize: (imageId, size) => request(`/images/${encodeURIComponent(imageId)}/background/${encodeURIComponent(size)}`),
|
|
241
|
+
getChoiceBySize: (imageId, size) => request(`/images/${encodeURIComponent(imageId)}/choice/${encodeURIComponent(size)}`),
|
|
242
|
+
delete: (imageId) => request(`/images/${encodeURIComponent(imageId)}`, { method: "DELETE" })
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function videosApi(request) {
|
|
246
|
+
return { upload: (body) => request("/videos", {
|
|
247
|
+
method: "POST",
|
|
248
|
+
body
|
|
249
|
+
}) };
|
|
250
|
+
}
|
|
251
|
+
function workspacesApi(request) {
|
|
252
|
+
return {
|
|
253
|
+
list: (query) => request("/workspaces", { query }),
|
|
254
|
+
get: (workspaceId) => request(`/workspaces/${encodeURIComponent(workspaceId)}`),
|
|
255
|
+
create: (body) => request("/workspaces", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
body
|
|
258
|
+
}),
|
|
259
|
+
update: (workspaceId, body) => request(`/workspaces/${encodeURIComponent(workspaceId)}`, {
|
|
260
|
+
method: "PATCH",
|
|
261
|
+
body
|
|
262
|
+
}),
|
|
263
|
+
delete: (workspaceId) => request(`/workspaces/${encodeURIComponent(workspaceId)}`, { method: "DELETE" })
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function webhooksApi(request) {
|
|
267
|
+
return {
|
|
268
|
+
list: (formId) => request(`/forms/${encodeURIComponent(formId)}/webhooks`),
|
|
269
|
+
get: (formId, tag) => request(`/forms/${encodeURIComponent(formId)}/webhooks/${encodeURIComponent(tag)}`),
|
|
270
|
+
createOrUpdate: (formId, tag, body) => request(`/forms/${encodeURIComponent(formId)}/webhooks/${encodeURIComponent(tag)}`, {
|
|
271
|
+
method: "PUT",
|
|
272
|
+
body
|
|
273
|
+
}),
|
|
274
|
+
delete: (formId, tag) => request(`/forms/${encodeURIComponent(formId)}/webhooks/${encodeURIComponent(tag)}`, { method: "DELETE" })
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function insightsApi(request) {
|
|
278
|
+
return {
|
|
279
|
+
getFormMessages: (formId) => request(`/forms/${encodeURIComponent(formId)}/messages`),
|
|
280
|
+
updateFormMessages: (formId, body) => request(`/forms/${encodeURIComponent(formId)}/messages`, {
|
|
281
|
+
method: "PUT",
|
|
282
|
+
body
|
|
283
|
+
})
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function createTypeformApiClient(accessToken, config) {
|
|
287
|
+
const request = makeRequest(accessToken, config);
|
|
288
|
+
return {
|
|
289
|
+
accounts: accountsApi(request),
|
|
290
|
+
forms: formsApi(request),
|
|
291
|
+
responses: responsesApi(request),
|
|
292
|
+
responseFiles: responseFilesApi(request),
|
|
293
|
+
themes: themesApi(request),
|
|
294
|
+
images: imagesApi(request),
|
|
295
|
+
videos: videosApi(request),
|
|
296
|
+
workspaces: workspacesApi(request),
|
|
297
|
+
webhooks: webhooksApi(request),
|
|
298
|
+
insights: insightsApi(request)
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function createTypeformClient(credentials, config = {}) {
|
|
302
|
+
const accessToken = credentials.TYPEFORM_ACCESS_TOKEN.trim();
|
|
303
|
+
if (accessToken.length === 0) throw new Error(`Typeform authentication failed while calling Typeform API \`client initialization\`. ${TYPEFORM_CONNECT_HINT}`);
|
|
304
|
+
return createErrorNormalizingProxy(createTypeformApiClient(accessToken, config), normalizeTypeformError);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
//#endregion
|
|
308
|
+
export { TypeformApiError, createLeakyBucketRateLimiter, createTypeformClient };
|