@tapi-dev/sdk 0.1.17 → 0.1.20
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 +393 -292
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +629 -142
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -1
- package/dist/late-input.d.ts +24 -0
- package/dist/late-input.js +112 -0
- package/dist/runs.d.ts +1 -0
- package/dist/runs.js +3 -0
- package/dist/sessions.d.ts +10 -0
- package/dist/sessions.js +14 -0
- package/dist/types.d.ts +45 -2
- package/dist/website-apis.d.ts +3 -1
- package/dist/website-apis.js +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,167 +10,266 @@ npm install @tapi-dev/sdk
|
|
|
10
10
|
|
|
11
11
|
This package is ESM-first and works in runtimes with `fetch`, including modern Node.js and browser-like server runtimes.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
13
|
+
The CLI command is `tapi`. If your package manager only installed the SDK
|
|
14
|
+
locally and `tapi` is not on `PATH`, use `npx tapi` as a fallback.
|
|
15
|
+
|
|
16
|
+
## Developer Flow
|
|
17
|
+
|
|
18
|
+
Tapi Studio is launched from the developer app repo. The repo's `.tapi/project.json`
|
|
19
|
+
is the project binding, so one developer can work on multiple Tapi projects from
|
|
20
|
+
different directories without a global Studio project picker.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @tapi-dev/sdk
|
|
24
|
+
tapi init --project brokerage
|
|
25
|
+
tapi studio
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`tapi studio` checks the required local `tapi-service`, installs it when needed,
|
|
29
|
+
downloads the portable Studio server release when the channel manifest uses
|
|
30
|
+
`installerKind: portable-server`, starts Studio locally, and opens the browser.
|
|
31
|
+
If the channel still publishes a legacy NSIS desktop Studio manifest and no
|
|
32
|
+
Studio executable is installed yet, `tapi studio` runs the installer first, then
|
|
33
|
+
opens Studio. The normal developer path is the portable server launched by the
|
|
34
|
+
CLI.
|
|
35
|
+
|
|
36
|
+
By default, Studio downloads and extracted portable server releases live under
|
|
37
|
+
the user's local app data directory. On Windows that is usually
|
|
38
|
+
`%LOCALAPPDATA%\Tapi\Studio`. To keep the whole Studio install on another drive,
|
|
39
|
+
set `TAPI_STUDIO_HOME` before launching:
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
$env:TAPI_STUDIO_HOME="F:\Tapi\Studio"
|
|
43
|
+
tapi studio
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For separate paths, use `--cache-dir` for downloaded zip artifacts and
|
|
47
|
+
`--install-dir` for extracted portable Studio server releases.
|
|
48
|
+
|
|
49
|
+
After a successful install, the CLI prunes older Tapi-managed download artifacts
|
|
50
|
+
from the cache and older SDK-marked release folders from the install directory.
|
|
51
|
+
The current artifact and current release are kept.
|
|
52
|
+
|
|
53
|
+
Useful commands:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
tapi init --project brokerage
|
|
57
|
+
tapi link --project brokerage
|
|
58
|
+
tapi studio
|
|
59
|
+
tapi service install --channel pilot
|
|
60
|
+
tapi service status
|
|
61
|
+
tapi apis generate
|
|
62
|
+
tapi sessions
|
|
63
|
+
tapi publish
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Local authoring files live in the app repo:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
.tapi/project.json
|
|
70
|
+
.tapi/sitemaps/<site>.json
|
|
71
|
+
.tapi/apis/<site>/<api>.json
|
|
72
|
+
.tapi/generated/catalog.json
|
|
73
|
+
src/tapi.generated.ts
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Drafts are local. `tapi publish` uploads local sitemaps and API contracts, marks
|
|
77
|
+
ready requests as published, and activates one immutable server release. Runtime
|
|
78
|
+
SDK calls read the active release catalog, not mutable Studio drafts.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
TAPI_API_KEY=tapi_project_key tapi publish
|
|
82
|
+
TAPI_API_KEY=tapi_project_key tapi apis generate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
Create one TAPI client in server-side app code. Use a project-scoped API key and
|
|
88
|
+
the same project id from `.tapi/project.json`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { TapiClient } from "@tapi-dev/sdk";
|
|
92
|
+
|
|
93
|
+
export const tapi = new TapiClient({
|
|
94
|
+
baseUrl: process.env.TAPI_BASE_URL!,
|
|
95
|
+
apiKey: process.env.TAPI_API_KEY!,
|
|
96
|
+
projectId: process.env.TAPI_PROJECT_ID!,
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Production is the default mode. Unknown website states fail the run instead of
|
|
101
|
+
holding a browser open:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const tapi = new TapiClient({
|
|
105
|
+
baseUrl: process.env.TAPI_BASE_URL!,
|
|
106
|
+
apiKey: process.env.TAPI_API_KEY!,
|
|
107
|
+
projectId: process.env.TAPI_PROJECT_ID!,
|
|
108
|
+
dev: false,
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
During development, set `dev: true`. If a website reaches an unknown state,
|
|
113
|
+
Tapi preserves the browser session for takeover instead of turning it into a
|
|
114
|
+
production failure:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const tapi = new TapiClient({
|
|
118
|
+
baseUrl: process.env.TAPI_BASE_URL!,
|
|
119
|
+
apiKey: process.env.TAPI_API_KEY!,
|
|
120
|
+
projectId: process.env.TAPI_PROJECT_ID!,
|
|
121
|
+
dev: true,
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Inspect preserved sessions from the repo:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
tapi sessions
|
|
129
|
+
tapi sessions --json
|
|
130
|
+
tapi sessions open <session-id>
|
|
79
131
|
```
|
|
80
132
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
the same project id from `.tapi/project.json`.
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { TapiClient } from "@tapi-dev/sdk";
|
|
88
|
-
|
|
89
|
-
export const tapi = new TapiClient({
|
|
90
|
-
baseUrl: process.env.TAPI_BASE_URL!,
|
|
91
|
-
apiKey: process.env.TAPI_API_KEY!,
|
|
92
|
-
projectId: process.env.TAPI_PROJECT_ID!,
|
|
93
|
-
});
|
|
94
|
-
```
|
|
133
|
+
`tapi sessions open` reuses the matching Studio window for the current repo when
|
|
134
|
+
one is already running; otherwise it installs/starts Studio and opens directly
|
|
135
|
+
to that session.
|
|
95
136
|
|
|
96
137
|
Generated website APIs are authored visually in Tapi Studio by setting workflow
|
|
97
138
|
bounds, selecting inputs/outputs, and publishing an API. The SDK sees the public
|
|
98
139
|
operation name:
|
|
99
|
-
|
|
100
|
-
```text
|
|
101
|
-
<namespace>.<operation>
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
const operation = await tapi.websiteApis.describe("schwab.place_order");
|
|
106
|
-
|
|
107
|
-
const run = await tapi.websiteApis.run("schwab.place_order", {
|
|
108
|
-
inputs: {
|
|
109
|
-
symbol: "AAPL",
|
|
110
|
-
side: "buy",
|
|
111
|
-
quantity: 10,
|
|
112
|
-
orderType: "limit",
|
|
113
|
-
limitPrice: 190,
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const completedRun = await tapi.runs.wait(run.id);
|
|
118
|
-
console.log(completedRun.status, completedRun.result);
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
##
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
<namespace>.<operation>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const operation = await tapi.websiteApis.describe("schwab.place_order");
|
|
147
|
+
|
|
148
|
+
const run = await tapi.websiteApis.run("schwab.place_order", {
|
|
149
|
+
inputs: {
|
|
150
|
+
symbol: "AAPL",
|
|
151
|
+
side: "buy",
|
|
152
|
+
quantity: 10,
|
|
153
|
+
orderType: "limit",
|
|
154
|
+
limitPrice: 190,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const completedRun = await tapi.runs.wait(run.id);
|
|
159
|
+
console.log(completedRun.status, completedRun.result);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Late Inputs
|
|
163
|
+
|
|
164
|
+
Most API inputs are known before the run starts. Use `lateInput()` when the
|
|
165
|
+
workflow needs a value later, after earlier browser steps have triggered work
|
|
166
|
+
somewhere else.
|
|
167
|
+
|
|
168
|
+
For example, state 4 might submit a login form and trigger a one-time code by
|
|
169
|
+
email or SMS. State 5 needs that code, but your app can only retrieve it after
|
|
170
|
+
state 4 runs. Declare the input up front with the API call, then resolve it
|
|
171
|
+
from your own code:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { lateInput } from "@tapi-dev/sdk";
|
|
175
|
+
|
|
176
|
+
const verificationCode = lateInput<string>({
|
|
177
|
+
resolve: async () => {
|
|
178
|
+
return await waitForVerificationCodeFromEmail();
|
|
179
|
+
},
|
|
180
|
+
timeoutMs: 180_000,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const run = await tapi.websiteApis.run("walmart.login", {
|
|
184
|
+
inputs: {
|
|
185
|
+
email: "buyer@example.com",
|
|
186
|
+
password: process.env.WALMART_PASSWORD!,
|
|
187
|
+
verificationCode,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await verificationCode.waitForProvides();
|
|
192
|
+
|
|
193
|
+
const completedRun = await tapi.runs.wait(run.id);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
You can also provide the value manually. This is useful when another callback,
|
|
197
|
+
worker, webhook, or polling loop finds the value:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const verificationCode = lateInput<string>({ timeoutMs: 180_000 });
|
|
201
|
+
|
|
202
|
+
const run = await tapi.websiteApis.run("walmart.login", {
|
|
203
|
+
inputs: { email: "buyer@example.com", verificationCode },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const code = await waitForVerificationCodeFromWebhook();
|
|
207
|
+
await verificationCode.set(code);
|
|
208
|
+
|
|
209
|
+
const completedRun = await tapi.runs.wait(run.id);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Tapi only waits when the runner reaches the action bound to that input. If your
|
|
213
|
+
app resolves the value early, Tapi stores it and uses it later. If the value is
|
|
214
|
+
not provided before `timeoutMs`, the run fails instead of guessing or using the
|
|
215
|
+
recorded default.
|
|
216
|
+
|
|
217
|
+
Late inputs are for values produced outside the website flow. Values read from
|
|
218
|
+
the website itself should stay as normal API outputs.
|
|
219
|
+
|
|
220
|
+
## API-Call Triggers
|
|
221
|
+
|
|
222
|
+
Triggers are scheduled calls to published website APIs. Keep trigger definitions
|
|
223
|
+
in the app repo and sync them after publishing the API catalog:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
// tapi.config.ts
|
|
227
|
+
export default {
|
|
228
|
+
triggers: {
|
|
229
|
+
nightlyBalance: {
|
|
230
|
+
apiRequest: "schwab.get_balance",
|
|
231
|
+
schedule: { cron: "0 9 * * MON-FRI" },
|
|
232
|
+
inputs: { accountId: "main" },
|
|
233
|
+
runtime: { profileRef: "perm_default" },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
TAPI_API_KEY=tapi_project_key tapi triggers sync
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`triggers sync` upserts by trigger name for the current `.tapi/project.json`
|
|
244
|
+
project. Schedules support standard five-field cron strings or intervals as
|
|
245
|
+
numbers of seconds / strings like `30s`, `15m`, `2h`, and `1d`.
|
|
246
|
+
|
|
247
|
+
You can also manage triggers directly:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
await tapi.triggers.create({
|
|
251
|
+
name: "nightlyBalance",
|
|
252
|
+
apiRequest: "schwab.get_balance",
|
|
253
|
+
schedule: { cron: "0 9 * * MON-FRI" },
|
|
254
|
+
inputs: { accountId: "main" },
|
|
255
|
+
runtime: { profileRef: "perm_default" },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await tapi.triggers.fire("act_123");
|
|
259
|
+
await tapi.triggers.disable("act_123");
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
You can inspect the same input/output contract from the CLI:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
tapi apis describe schwab.place_order \
|
|
266
|
+
--api-base-url "$TAPI_BASE_URL" \
|
|
267
|
+
--api-key "$TAPI_API_KEY" \
|
|
268
|
+
--project "$TAPI_PROJECT_ID"
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Do not expose `TAPI_API_KEY` in public browser bundles. Put the SDK behind your
|
|
272
|
+
own backend route, server action, or job worker when using secret API keys.
|
|
174
273
|
|
|
175
274
|
## Runtime Profiles and Proxies
|
|
176
275
|
|
|
@@ -241,17 +340,17 @@ const tapi = new TapiClient({
|
|
|
241
340
|
});
|
|
242
341
|
```
|
|
243
342
|
|
|
244
|
-
## Configuration
|
|
245
|
-
|
|
246
|
-
```env
|
|
247
|
-
TAPI_BASE_URL=https://your-tapi-api-host
|
|
248
|
-
TAPI_API_KEY=tapi_project_key
|
|
249
|
-
TAPI_PROJECT_ID=brokerage
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
`projectId` is optional only when the API key itself is project-scoped. If
|
|
253
|
-
provided, the SDK sends it as the `X-Tapi-Project` header and the server rejects
|
|
254
|
-
mismatches.
|
|
343
|
+
## Configuration
|
|
344
|
+
|
|
345
|
+
```env
|
|
346
|
+
TAPI_BASE_URL=https://your-tapi-api-host
|
|
347
|
+
TAPI_API_KEY=tapi_project_key
|
|
348
|
+
TAPI_PROJECT_ID=brokerage
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`projectId` is optional only when the API key itself is project-scoped. If
|
|
352
|
+
provided, the SDK sends it as the `X-Tapi-Project` header and the server rejects
|
|
353
|
+
mismatches.
|
|
255
354
|
|
|
256
355
|
## Common Project Setup
|
|
257
356
|
|
|
@@ -267,12 +366,12 @@ src/
|
|
|
267
366
|
// src/lib/tapi.ts
|
|
268
367
|
import { TapiClient } from "@tapi-dev/sdk";
|
|
269
368
|
|
|
270
|
-
export const tapi = new TapiClient({
|
|
271
|
-
baseUrl: process.env.TAPI_BASE_URL!,
|
|
272
|
-
apiKey: process.env.TAPI_API_KEY!,
|
|
273
|
-
projectId: process.env.TAPI_PROJECT_ID!,
|
|
274
|
-
});
|
|
275
|
-
```
|
|
369
|
+
export const tapi = new TapiClient({
|
|
370
|
+
baseUrl: process.env.TAPI_BASE_URL!,
|
|
371
|
+
apiKey: process.env.TAPI_API_KEY!,
|
|
372
|
+
projectId: process.env.TAPI_PROJECT_ID!,
|
|
373
|
+
});
|
|
374
|
+
```
|
|
276
375
|
|
|
277
376
|
Application code should import this shared client instead of constructing a new client in every file.
|
|
278
377
|
|
|
@@ -297,126 +396,127 @@ const run = await tapi.websiteApis.run("apiName.requestKey", {
|
|
|
297
396
|
});
|
|
298
397
|
|
|
299
398
|
await tapi.runs.get(run.id);
|
|
300
|
-
await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
|
|
301
|
-
await tapi.runs.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{ symbol: "
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{ symbol: "
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
`
|
|
384
|
-
`
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
399
|
+
await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
|
|
400
|
+
await tapi.runs.provideInput(run.id, "verificationCode", "123456");
|
|
401
|
+
await tapi.runs.cancel(run.id);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Cloud Batch Runs
|
|
405
|
+
|
|
406
|
+
Cloud runs are requested from the SDK, but VM provisioning, service
|
|
407
|
+
installation, worker leases, browser internals, and AWS cleanup stay on the
|
|
408
|
+
Tapi server. The SDK is only the front door.
|
|
409
|
+
|
|
410
|
+
Keep this code server-side. Do not put `TAPI_API_KEY` in a browser bundle and
|
|
411
|
+
do not wire AWS or Stripe from the developer's app. The app sends its own user
|
|
412
|
+
identity to Tapi, and Tapi handles prepaid Stripe checkout, credit accounting,
|
|
413
|
+
AWS worker provisioning, and cleanup.
|
|
414
|
+
|
|
415
|
+
The minimum developer flow is:
|
|
416
|
+
|
|
417
|
+
1. App user clicks a button.
|
|
418
|
+
2. The developer's backend calls `runCloudBatch` with `user.externalUserId`.
|
|
419
|
+
3. If the returned run has `status: "payment_required"`, redirect the app user
|
|
420
|
+
to `run.checkoutUrl`.
|
|
421
|
+
4. Stripe calls the Tapi webhook, Tapi credits that same `externalUserId`, and
|
|
422
|
+
the app retries the cloud batch.
|
|
423
|
+
5. If credit is available, Tapi reserves the balance and starts cloud workers.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
// backend route or server action
|
|
427
|
+
const quote = await tapi.websiteApis.quoteCloud("schwab.place_order", {
|
|
428
|
+
inputs: [
|
|
429
|
+
{ symbol: "AAPL", quantity: 1 },
|
|
430
|
+
{ symbol: "MSFT", quantity: 2 },
|
|
431
|
+
],
|
|
432
|
+
user: {
|
|
433
|
+
externalUserId: appUser.id,
|
|
434
|
+
email: appUser.email,
|
|
435
|
+
},
|
|
436
|
+
cloud: {
|
|
437
|
+
windows: true,
|
|
438
|
+
maxVms: 1,
|
|
439
|
+
chromePerVm: 10,
|
|
440
|
+
maxCostUsd: 20,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
if (!quote.withinMaxCost) {
|
|
445
|
+
throw new Error("Cloud batch exceeds the configured cost cap");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const run = await tapi.websiteApis.runCloudBatch("schwab.place_order", {
|
|
449
|
+
inputs: [
|
|
450
|
+
{ symbol: "AAPL", quantity: 1 },
|
|
451
|
+
{ symbol: "MSFT", quantity: 2 },
|
|
452
|
+
],
|
|
453
|
+
user: {
|
|
454
|
+
externalUserId: appUser.id,
|
|
455
|
+
email: appUser.email,
|
|
456
|
+
},
|
|
457
|
+
payment: {
|
|
458
|
+
successUrl: `https://your-app.example/cloud/success`,
|
|
459
|
+
cancelUrl: `https://your-app.example/cloud/cancel`,
|
|
460
|
+
},
|
|
461
|
+
cloud: {
|
|
462
|
+
windows: true,
|
|
463
|
+
maxVms: 1,
|
|
464
|
+
chromePerVm: 10,
|
|
465
|
+
maxCostUsd: 20,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (run.status === "payment_required") {
|
|
470
|
+
return { redirectTo: run.checkoutUrl };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for await (const event of tapi.cloudRuns.stream(run.id)) {
|
|
474
|
+
console.log(event.status, event.traceId);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const results = await tapi.cloudRuns.results(run.id);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
When `CLOUD_BILLING_ENABLED=true`, the server refuses to launch AWS workers
|
|
481
|
+
unless prepaid cloud credit is available and reserved first. If balance is too
|
|
482
|
+
low, `runCloudBatch` converts the server's HTTP `402` into a normal
|
|
483
|
+
`CloudBatchRun` object with `status: "payment_required"`, `checkoutUrl`,
|
|
484
|
+
`availableBalanceCents`, and `requiredBalanceCents`. That keeps the button
|
|
485
|
+
handler simple.
|
|
486
|
+
|
|
487
|
+
Credits are scoped under the Tapi API key owner, app id, and
|
|
488
|
+
`user.externalUserId`. Use the same `externalUserId` for quote, balance,
|
|
489
|
+
checkout, and run calls. If you omit `externalUserId` but provide `email`, Tapi
|
|
490
|
+
uses the normalized email as the external user id.
|
|
491
|
+
|
|
492
|
+
Applications can also create a top-up checkout explicitly:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
const user = {
|
|
496
|
+
externalUserId: appUser.id,
|
|
497
|
+
email: appUser.email,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const balance = await tapi.cloudRuns.balance(user);
|
|
501
|
+
|
|
502
|
+
if (balance.balanceCents < balance.minimumBalanceCents) {
|
|
503
|
+
const checkout = await tapi.cloudRuns.checkout({
|
|
504
|
+
amountCents: balance.minimumBalanceCents,
|
|
505
|
+
user,
|
|
506
|
+
successUrl: "https://your-app.example/cloud/success",
|
|
507
|
+
cancelUrl: "https://your-app.example/cloud/cancel",
|
|
508
|
+
});
|
|
509
|
+
console.log(checkout.checkoutUrl);
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The developer application does not need VM provisioning logic, installer
|
|
514
|
+
deployment logic, worker command interpretation, browser runner internals, or
|
|
515
|
+
builder logic. Those stay behind the Tapi server API.
|
|
516
|
+
|
|
517
|
+
Website API requests are addressed as `<namespace>.<operation>`. The namespace
|
|
518
|
+
comes from the generated website name in Studio. The operation is the name the
|
|
519
|
+
developer types into the SDK name textbox at the workflow boundary.
|
|
420
520
|
|
|
421
521
|
`describe()` returns the public SDK contract: input controls such as
|
|
422
522
|
`textbox`, `radio`, `select`, `checkbox`, conditional requirements such as
|
|
@@ -446,6 +546,7 @@ The package includes generated TypeScript declarations. Common exported types in
|
|
|
446
546
|
|
|
447
547
|
```ts
|
|
448
548
|
import type {
|
|
549
|
+
LateInput,
|
|
449
550
|
RuntimeRequirements,
|
|
450
551
|
RuntimeProfile,
|
|
451
552
|
RuntimeRunOptions,
|