@tapi-dev/sdk 0.1.5 → 0.1.7

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 CHANGED
@@ -14,72 +14,72 @@ This package is ESM-first and works in runtimes with `fetch`, including modern N
14
14
 
15
15
  Tapi Studio is the desktop app used to author and test local Tapi integrations. Install the SDK first, then use the bundled CLI:
16
16
 
17
- ```bash
18
- npm install @tapi-dev/sdk
19
- npx tapi studio install --channel pilot
20
- npx tapi studio open
21
- ```
22
-
23
- `studio install` opens a browser sign-in before any Studio EXE download. The
24
- CLI exchanges the browser credential for Firebase auth, requests a short-lived
25
- Studio install token from the Tapi API, then fetches a protected manifest and
26
- installer download URL. If the signed-in user is not yet approved, the server
27
- submits an approval request and sends the existing email approval links to the
28
- admin. After approval, rerun the same command.
29
-
30
- You can also run the CLI directly from npm without adding the package first:
31
-
32
- ```bash
33
- npx @tapi-dev/sdk studio install --channel pilot
34
- ```
35
-
36
- The install flow talks to the Tapi API host first:
37
-
38
- ```text
39
- POST /api/sdk/v1/studio/install-token
40
- GET /api/sdk/v1/studio/releases/<channel>
41
- ```
42
-
43
- It then downloads the Windows installer, verifies the manifest SHA256, caches
44
- the installer locally, and runs it. The installer cache defaults to:
17
+ ```bash
18
+ npm install @tapi-dev/sdk
19
+ npx tapi studio install --channel pilot
20
+ npx tapi studio open
21
+ ```
22
+
23
+ `studio install` opens a browser sign-in before any Studio EXE download. The
24
+ CLI exchanges the browser credential for Firebase auth, requests a short-lived
25
+ Studio install token from the Tapi API, then fetches a protected manifest and
26
+ installer download URL. If the signed-in user is not yet approved, the server
27
+ submits an approval request and sends the existing email approval links to the
28
+ admin. After approval, rerun the same command.
29
+
30
+ You can also run the CLI directly from npm without adding the package first:
31
+
32
+ ```bash
33
+ npx @tapi-dev/sdk studio install --channel pilot
34
+ ```
35
+
36
+ The install flow talks to the Tapi API host first:
45
37
 
46
38
  ```text
47
- %LOCALAPPDATA%\Tapi\Studio\downloads
48
- ```
49
-
50
- Tapi Studio includes the matching Tapi Service build. When the installer runs,
51
- it installs or replaces the local `tapi-service` Windows service with the
52
- version declared in the Studio manifest. Developers do not pick a service
53
- version separately; updating Studio updates the service version used by that
54
- project and by apps generated from that project.
55
-
56
- Useful commands:
57
-
58
- ```bash
59
- npx tapi studio install --channel pilot
60
- npx tapi studio install --channel pilot --download-only
61
- npx tapi studio install --api-base-url https://api.example.com
62
- npx tapi studio install --install-token tsi_your_preissued_token
63
- npx tapi studio open
64
- npx tapi studio doctor
65
- ```
66
-
67
- Environment overrides:
68
-
69
- ```env
70
- TAPI_BASE_URL=https://your-tapi-api-host
71
- TAPI_STUDIO_API_BASE_URL=https://your-tapi-api-host
72
- TAPI_STUDIO_CHANNEL=pilot
73
- TAPI_STUDIO_MANIFEST_URL=https://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
74
- TAPI_STUDIO_INSTALL_TOKEN=tsi_your_preissued_token
75
- TAPI_STUDIO_AUTH_HTML_URL=https://rsarlong-1f92fd.gitlab.io/auth.html
76
- TAPI_FIREBASE_API_KEY=AIzaSyCDZR8lWyVQcWYfFdNZa4vuL4IWEC0h6gE
77
- TAPI_STUDIO_EXE=C:\Users\you\AppData\Local\Tapi Studio\Tapi Studio.exe
78
- ```
79
-
80
- `TAPI_STUDIO_MANIFEST_URL` remains useful for `studio doctor`. Direct manifest
81
- overrides are not used by the protected installer path.
82
-
39
+ POST /api/sdk/v1/studio/install-token
40
+ GET /api/sdk/v1/studio/releases/<channel>
41
+ ```
42
+
43
+ It then downloads the Windows installer, verifies the manifest SHA256, caches
44
+ the installer locally, and runs it. The installer cache defaults to:
45
+
46
+ ```text
47
+ %LOCALAPPDATA%\Tapi\Studio\downloads
48
+ ```
49
+
50
+ Tapi Studio includes the matching Tapi Service build. When the installer runs,
51
+ it installs or replaces the local `tapi-service` Windows service with the
52
+ version declared in the Studio manifest. Developers do not pick a service
53
+ version separately; updating Studio updates the service version used by that
54
+ project and by apps generated from that project.
55
+
56
+ Useful commands:
57
+
58
+ ```bash
59
+ npx tapi studio install --channel pilot
60
+ npx tapi studio install --channel pilot --download-only
61
+ npx tapi studio install --api-base-url https://api.example.com
62
+ npx tapi studio install --install-token tsi_your_preissued_token
63
+ npx tapi studio open
64
+ npx tapi studio doctor
65
+ ```
66
+
67
+ Environment overrides:
68
+
69
+ ```env
70
+ TAPI_BASE_URL=https://your-tapi-api-host
71
+ TAPI_STUDIO_API_BASE_URL=https://your-tapi-api-host
72
+ TAPI_STUDIO_CHANNEL=pilot
73
+ TAPI_STUDIO_MANIFEST_URL=https://d4xaf52nfwiok.cloudfront.net/studio/channels/pilot/latest.json
74
+ TAPI_STUDIO_INSTALL_TOKEN=tsi_your_preissued_token
75
+ TAPI_STUDIO_AUTH_HTML_URL=https://rsarlong-1f92fd.gitlab.io/auth.html
76
+ TAPI_FIREBASE_API_KEY=AIzaSyCDZR8lWyVQcWYfFdNZa4vuL4IWEC0h6gE
77
+ TAPI_STUDIO_EXE=C:\Users\you\AppData\Local\Tapi Studio\Tapi Studio.exe
78
+ ```
79
+
80
+ `TAPI_STUDIO_MANIFEST_URL` remains useful for `studio doctor`. Direct manifest
81
+ overrides are not used by the protected installer path.
82
+
83
83
  Server-side protected install delivery also requires:
84
84
 
85
85
  ```env
@@ -92,6 +92,17 @@ If the Studio installer objects remain publicly downloadable, users can bypass
92
92
  the approval gate by skipping the CLI entirely.
93
93
 
94
94
  Tapi Studio desktop releases are currently published for Windows x64.
95
+
96
+ Cloud Windows workers use a separate protected service runtime manifest:
97
+
98
+ ```text
99
+ GET /api/sdk/v1/service/releases/<channel>
100
+ ```
101
+
102
+ The request must include either `Authorization: Bearer
103
+ $CLOUD_WORKER_BOOTSTRAP_SECRET` or `X-Tapi-Worker-Bootstrap-Secret`. The server
104
+ reads `service/channels/<channel>/latest.json` from the private S3 bucket and
105
+ returns a short-lived signed download URL for the Tapi Service zip.
95
106
 
96
107
  ## Quick Start
97
108
 
@@ -122,8 +133,111 @@ const completedRun = await tapi.runs.wait(run.id);
122
133
  console.log(completedRun.status, completedRun.result);
123
134
  ```
124
135
 
136
+ Generated website APIs are created in Tapi Studio by setting workflow bounds,
137
+ choosing the SDK operation name, selecting inputs/outputs, and publishing the
138
+ operation. Studio owns the private workflow bindings. The SDK sees the public
139
+ shape as:
140
+
141
+ ```text
142
+ <autogenerated website namespace>.<developer operation name>
143
+ ```
144
+
145
+ For example, if Studio shows `SDK name: schwab.[ place_order ]`, call:
146
+
147
+ ```ts
148
+ const operation = await tapi.websiteApis.describe("schwab.place_order");
149
+
150
+ const run = await tapi.websiteApis.run("schwab.place_order", {
151
+ inputs: {
152
+ symbol: "AAPL",
153
+ side: "buy",
154
+ quantity: 10,
155
+ orderType: "limit",
156
+ limitPrice: 190,
157
+ },
158
+ });
159
+ ```
160
+
161
+ You can inspect the same input/output contract from the CLI:
162
+
163
+ ```bash
164
+ npx tapi apis describe schwab.place_order \
165
+ --api-base-url "$TAPI_BASE_URL" \
166
+ --api-key "$TAPI_API_KEY" \
167
+ --app "$TAPI_APP_ID"
168
+ ```
169
+
125
170
  Do not expose `TAPI_API_KEY` in public browser bundles. Put the SDK behind your own backend route, server action, or job worker when using secret API keys.
126
171
 
172
+ ## Runtime Profiles and Proxies
173
+
174
+ Browser identity is selected with a runner-local `profileRef`, not by passing
175
+ Chrome folder paths through the cloud API.
176
+
177
+ Use `PermProfile` for a long-lived account identity. It owns a persistent
178
+ Chrome user-data directory and should usually use the runner's normal home IP:
179
+
180
+ ```ts
181
+ const perm = await tapi.runtime.permProfiles.create({
182
+ displayName: "Mom Walmart",
183
+ originPolicy: { type: "home" },
184
+ });
185
+
186
+ await tapi.runtime.permProfiles.launchSetup(perm.profile.profileRef);
187
+ // The user logs in, adds passkeys, then closes setup Chrome.
188
+
189
+ await tapi.websiteApis.run("walmart.reorder", {
190
+ inputs: { item: "paper towels" },
191
+ runtime: { profileRef: perm.profile.profileRef },
192
+ });
193
+ ```
194
+
195
+ Use `TempProfile` for throwaway work. Temp profiles can be cloned from a
196
+ template profile and assigned different proxies:
197
+
198
+ ```ts
199
+ const temp = await tapi.runtime.tempProfiles.provisionMany({
200
+ count: 3,
201
+ displayNamePrefix: "checkout",
202
+ proxies: [
203
+ "http://user1:pass1@proxy-a.example:8080",
204
+ "http://user2:pass2@proxy-b.example:8080",
205
+ "http://user3:pass3@proxy-c.example:8080",
206
+ ],
207
+ destroyOnRelease: true,
208
+ });
209
+
210
+ await Promise.all(
211
+ temp.profiles.map((profile) =>
212
+ tapi.websiteApis.run("walmart.lookupItem", {
213
+ inputs: { sku: "123" },
214
+ runtime: {
215
+ profileRef: profile.profileRef,
216
+ destroyOnRelease: profile.destroyOnRelease,
217
+ },
218
+ }),
219
+ ),
220
+ );
221
+ ```
222
+
223
+ `runtime.profileRef` is the only value sent to the TAPI cloud run API. Proxy
224
+ URLs, proxy credentials, and Chrome profile folders stay on the local runner
225
+ and are handled through the local control WebSocket at `ws://127.0.0.1:8765`.
226
+ When the same `PermProfile` is used concurrently for multiple different sites,
227
+ the runner creates runtime clones so Chrome does not open the same
228
+ `--user-data-dir` twice. You can also create a clone explicitly with
229
+ `tapi.runtime.profiles.clone({ profileRef, site })`, but most apps should pass
230
+ the base `profileRef` and let the runner decide.
231
+ If your JavaScript runtime does not provide `WebSocket`, pass one:
232
+
233
+ ```ts
234
+ const tapi = new TapiClient({
235
+ baseUrl: process.env.TAPI_BASE_URL!,
236
+ apiKey: process.env.TAPI_API_KEY!,
237
+ webSocket: MyWebSocketImplementation,
238
+ });
239
+ ```
240
+
127
241
  ## Configuration
128
242
 
129
243
  ```env
@@ -163,20 +277,146 @@ Application code should import this shared client instead of constructing a new
163
277
  await tapi.catalog.get();
164
278
  await tapi.runners.list();
165
279
  await tapi.runtime.requirements();
280
+ await tapi.runtime.profiles.list();
281
+ await tapi.runtime.profiles.clone({ profileRef: "perm_default", site: "walmart" });
282
+ await tapi.runtime.permProfiles.create({ displayName: "Default", originPolicy: { type: "home" } });
283
+ await tapi.runtime.tempProfiles.provisionMany({ proxies: ["http://user:pass@host:8080"] });
284
+ await tapi.websiteApis.describe("schwab.place_order");
166
285
 
167
286
  const run = await tapi.websiteApis.run("apiName.requestKey", {
168
287
  inputs: { example: true },
288
+ runtime: { profileRef: "perm_default" },
169
289
  priority: 5,
170
290
  runnerId: "runner-id",
171
291
  idempotencyKey: "request-123",
172
292
  });
173
293
 
174
294
  await tapi.runs.get(run.id);
175
- await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
176
- await tapi.runs.cancel(run.id);
177
- ```
295
+ await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
296
+ await tapi.runs.cancel(run.id);
297
+ ```
298
+
299
+ ## Cloud Batch Runs
300
+
301
+ Cloud runs are requested from the SDK, but VM provisioning, service
302
+ installation, worker leases, browser internals, and AWS cleanup stay on the
303
+ Tapi server. The SDK is only the front door.
304
+
305
+ Keep this code server-side. Do not put `TAPI_API_KEY` in a browser bundle and
306
+ do not wire AWS or Stripe from the developer's app. The app sends its own user
307
+ identity to Tapi, and Tapi handles prepaid Stripe checkout, credit accounting,
308
+ AWS worker provisioning, and cleanup.
309
+
310
+ The minimum developer flow is:
311
+
312
+ 1. App user clicks a button.
313
+ 2. The developer's backend calls `runCloudBatch` with `user.externalUserId`.
314
+ 3. If the returned run has `status: "payment_required"`, redirect the app user
315
+ to `run.checkoutUrl`.
316
+ 4. Stripe calls the Tapi webhook, Tapi credits that same `externalUserId`, and
317
+ the app retries the cloud batch.
318
+ 5. If credit is available, Tapi reserves the balance and starts cloud workers.
319
+
320
+ ```ts
321
+ // backend route or server action
322
+ const quote = await tapi.websiteApis.quoteCloud("schwab.place_order", {
323
+ inputs: [
324
+ { symbol: "AAPL", quantity: 1 },
325
+ { symbol: "MSFT", quantity: 2 },
326
+ ],
327
+ user: {
328
+ externalUserId: appUser.id,
329
+ email: appUser.email,
330
+ },
331
+ cloud: {
332
+ windows: true,
333
+ maxVms: 1,
334
+ chromePerVm: 10,
335
+ maxCostUsd: 20,
336
+ },
337
+ });
338
+
339
+ if (!quote.withinMaxCost) {
340
+ throw new Error("Cloud batch exceeds the configured cost cap");
341
+ }
342
+
343
+ const run = await tapi.websiteApis.runCloudBatch("schwab.place_order", {
344
+ inputs: [
345
+ { symbol: "AAPL", quantity: 1 },
346
+ { symbol: "MSFT", quantity: 2 },
347
+ ],
348
+ user: {
349
+ externalUserId: appUser.id,
350
+ email: appUser.email,
351
+ },
352
+ payment: {
353
+ successUrl: `https://your-app.example/cloud/success`,
354
+ cancelUrl: `https://your-app.example/cloud/cancel`,
355
+ },
356
+ cloud: {
357
+ windows: true,
358
+ maxVms: 1,
359
+ chromePerVm: 10,
360
+ maxCostUsd: 20,
361
+ },
362
+ });
363
+
364
+ if (run.status === "payment_required") {
365
+ return { redirectTo: run.checkoutUrl };
366
+ }
367
+
368
+ for await (const event of tapi.cloudRuns.stream(run.id)) {
369
+ console.log(event.status, event.traceId);
370
+ }
371
+
372
+ const results = await tapi.cloudRuns.results(run.id);
373
+ ```
374
+
375
+ When `CLOUD_BILLING_ENABLED=true`, the server refuses to launch AWS workers
376
+ unless prepaid cloud credit is available and reserved first. If balance is too
377
+ low, `runCloudBatch` converts the server's HTTP `402` into a normal
378
+ `CloudBatchRun` object with `status: "payment_required"`, `checkoutUrl`,
379
+ `availableBalanceCents`, and `requiredBalanceCents`. That keeps the button
380
+ handler simple.
381
+
382
+ Credits are scoped under the Tapi API key owner, app id, and
383
+ `user.externalUserId`. Use the same `externalUserId` for quote, balance,
384
+ checkout, and run calls. If you omit `externalUserId` but provide `email`, Tapi
385
+ uses the normalized email as the external user id.
386
+
387
+ Applications can also create a top-up checkout explicitly:
388
+
389
+ ```ts
390
+ const user = {
391
+ externalUserId: appUser.id,
392
+ email: appUser.email,
393
+ };
394
+
395
+ const balance = await tapi.cloudRuns.balance(user);
396
+
397
+ if (balance.balanceCents < balance.minimumBalanceCents) {
398
+ const checkout = await tapi.cloudRuns.checkout({
399
+ amountCents: balance.minimumBalanceCents,
400
+ user,
401
+ successUrl: "https://your-app.example/cloud/success",
402
+ cancelUrl: "https://your-app.example/cloud/cancel",
403
+ });
404
+ console.log(checkout.checkoutUrl);
405
+ }
406
+ ```
407
+
408
+ The developer application does not need VM provisioning logic, installer
409
+ deployment logic, worker command interpretation, browser runner internals, or
410
+ builder logic. Those stay behind the Tapi server API.
411
+
412
+ Website API requests are addressed as `<namespace>.<operation>`. The namespace
413
+ comes from the generated website name in Studio. The operation is the name the
414
+ developer types into the SDK name textbox at the workflow boundary.
178
415
 
179
- Website API requests are addressed as `<apiName>.<requestKey>`.
416
+ `describe()` returns the public SDK contract: input controls such as
417
+ `textbox`, `radio`, `select`, `checkbox`, conditional requirements such as
418
+ `requiredWhen`, and the output schema. It does not expose workflow ids, state
419
+ ids, action ids, selectors, runner ids, or other Studio internals.
180
420
 
181
421
  ## Errors
182
422
 
@@ -202,9 +442,12 @@ The package includes generated TypeScript declarations. Common exported types in
202
442
  ```ts
203
443
  import type {
204
444
  RuntimeRequirements,
445
+ RuntimeProfile,
446
+ RuntimeRunOptions,
205
447
  SdkCatalog,
206
448
  TapiRun,
207
449
  TapiRunner,
450
+ WebsiteApiOperation,
208
451
  WebsiteApiRunRequest,
209
452
  } from "@tapi-dev/sdk";
210
453
  ```
package/dist/catalog.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { HttpClient } from "./client";
2
- import type { SdkCatalog } from "./types";
1
+ import type { HttpClient } from "./client.js";
2
+ import type { SdkCatalog } from "./types.js";
3
3
  export declare class CatalogResource {
4
4
  private readonly http;
5
5
  constructor(http: HttpClient);
package/dist/cli.d.ts CHANGED
@@ -20,6 +20,10 @@ export interface StudioReleaseManifest {
20
20
  version?: string;
21
21
  source?: string;
22
22
  manifestUrl?: string;
23
+ mobileChrome?: {
24
+ source?: string;
25
+ executable?: string;
26
+ };
23
27
  };
24
28
  }
25
29
  interface StudioCliOptions {
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { performance } from "node:perf_hooks";
10
10
  import { Readable } from "node:stream";
11
11
  import { pipeline } from "node:stream/promises";
12
12
  import { fileURLToPath } from "node:url";
13
+ import { TapiClient } from "./index.js";
13
14
  const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
14
15
  const DEFAULT_STUDIO_API_BASE_URL = "https://determined-motivation-production.up.railway.app";
15
16
  const DEFAULT_STUDIO_AUTH_HTML_URL = "https://rsarlong-1f92fd.gitlab.io/auth.html";
@@ -46,6 +47,18 @@ export async function runCli(argv = process.argv.slice(2)) {
46
47
  }
47
48
  return runDoctor(parseStudioOptions([subcommand, ...rest].filter(Boolean)));
48
49
  }
50
+ if (command === "apis") {
51
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
52
+ printApisHelp();
53
+ return 0;
54
+ }
55
+ if (subcommand === "describe") {
56
+ return describeApiOperation(rest);
57
+ }
58
+ console.error(`Unknown apis command: ${subcommand}`);
59
+ printApisHelp();
60
+ return 1;
61
+ }
49
62
  if (command !== "studio") {
50
63
  console.error(`Unknown command: ${command}`);
51
64
  printHelp();
@@ -168,6 +181,84 @@ export function parseStudioOptions(args) {
168
181
  installToken: raw.installToken ?? envString("TAPI_STUDIO_INSTALL_TOKEN"),
169
182
  };
170
183
  }
184
+ function parseApiOptions(args) {
185
+ let operation = "";
186
+ let apiBaseUrl = envString("TAPI_BASE_URL") || DEFAULT_STUDIO_API_BASE_URL;
187
+ let apiKey = envString("TAPI_API_KEY") || "";
188
+ let appId = envString("TAPI_APP_ID");
189
+ for (let index = 0; index < args.length; index += 1) {
190
+ const arg = args[index];
191
+ if (!arg)
192
+ continue;
193
+ if (arg === "--api-base-url" || arg === "--server") {
194
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
195
+ continue;
196
+ }
197
+ if (arg.startsWith("--api-base-url=")) {
198
+ apiBaseUrl = arg.slice("--api-base-url=".length);
199
+ continue;
200
+ }
201
+ if (arg.startsWith("--server=")) {
202
+ apiBaseUrl = arg.slice("--server=".length);
203
+ continue;
204
+ }
205
+ if (arg === "--api-key") {
206
+ apiKey = requireOptionValue(args, ++index, "--api-key");
207
+ continue;
208
+ }
209
+ if (arg.startsWith("--api-key=")) {
210
+ apiKey = arg.slice("--api-key=".length);
211
+ continue;
212
+ }
213
+ if (arg === "--app") {
214
+ appId = requireOptionValue(args, ++index, "--app");
215
+ continue;
216
+ }
217
+ if (arg.startsWith("--app=")) {
218
+ appId = arg.slice("--app=".length);
219
+ continue;
220
+ }
221
+ if (arg.startsWith("--")) {
222
+ throw new Error(`Unknown apis option: ${arg}`);
223
+ }
224
+ if (!operation) {
225
+ operation = arg;
226
+ continue;
227
+ }
228
+ throw new Error(`Unexpected apis argument: ${arg}`);
229
+ }
230
+ if (!operation) {
231
+ throw new Error("apis describe requires an operation like schwab.place_order.");
232
+ }
233
+ if (!apiKey) {
234
+ throw new Error("apis describe requires --api-key or TAPI_API_KEY.");
235
+ }
236
+ return {
237
+ operation,
238
+ options: {
239
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
240
+ apiKey,
241
+ appId,
242
+ },
243
+ };
244
+ }
245
+ async function describeApiOperation(args) {
246
+ try {
247
+ const { operation, options } = parseApiOptions(args);
248
+ const client = new TapiClient({
249
+ baseUrl: options.apiBaseUrl,
250
+ apiKey: options.apiKey,
251
+ appId: options.appId,
252
+ });
253
+ const description = await client.websiteApis.describe(operation);
254
+ console.log(JSON.stringify(description, null, 2));
255
+ return 0;
256
+ }
257
+ catch (error) {
258
+ console.error(formatError(error));
259
+ return 1;
260
+ }
261
+ }
171
262
  export function getDefaultStudioCacheDir() {
172
263
  if (process.platform === "win32") {
173
264
  const localAppData = process.env.LOCALAPPDATA ??
@@ -252,9 +343,8 @@ export function getCliWideEventRoot() {
252
343
  return override;
253
344
  }
254
345
  if (process.platform === "win32") {
255
- const localAppData = process.env.LOCALAPPDATA ??
256
- (process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
257
- return join(localAppData, "Tapi", "logs", "events");
346
+ const programData = process.env.PROGRAMDATA?.trim() || "C:\\ProgramData";
347
+ return join(programData, "Tapi", "logs", "events");
258
348
  }
259
349
  return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "tapi", "logs", "events");
260
350
  }
@@ -555,10 +645,14 @@ async function fetchProtectedStudioManifest(apiBaseUrl, channel, installToken, f
555
645
  Authorization: `Bearer ${installToken}`,
556
646
  },
557
647
  });
648
+ const responseBody = await readJsonBody(response);
558
649
  if (!response.ok) {
559
- throw new Error(`Failed to fetch protected Studio manifest: HTTP ${response.status}`);
650
+ const detail = typeof responseBody?.detail === "string" ? responseBody.detail : "";
651
+ throw new Error(detail
652
+ ? `Failed to fetch protected Studio manifest: HTTP ${response.status} (${detail})`
653
+ : `Failed to fetch protected Studio manifest: HTTP ${response.status}`);
560
654
  }
561
- return validateStudioManifest(await response.json());
655
+ return validateStudioManifest(responseBody);
562
656
  }
563
657
  async function fetchStudioManifest(manifestUrl, fetchImpl = fetch) {
564
658
  const response = await fetchImpl(manifestUrl, {
@@ -971,39 +1065,54 @@ function readSdkVersion() {
971
1065
  return packageJson.version;
972
1066
  }
973
1067
  function printHelp() {
974
- console.log(`Tapi CLI
975
-
976
- Usage:
977
- tapi studio install [--channel pilot] [--api-base-url URL]
978
- tapi studio open
979
- tapi studio doctor
980
- tapi doctor
981
-
982
- Commands:
983
- studio install Download, verify, and run the Tapi Studio installer
984
- studio open Open an installed Tapi Studio desktop app
985
- studio doctor Check local SDK and Studio release configuration
986
- doctor Alias for studio doctor
1068
+ console.log(`Tapi CLI
1069
+
1070
+ Usage:
1071
+ tapi studio install [--channel pilot] [--api-base-url URL]
1072
+ tapi studio open
1073
+ tapi studio doctor
1074
+ tapi apis describe <namespace.operation>
1075
+ tapi doctor
1076
+
1077
+ Commands:
1078
+ studio install Download, verify, and run the Tapi Studio installer
1079
+ studio open Open an installed Tapi Studio desktop app
1080
+ studio doctor Check local SDK and Studio release configuration
1081
+ apis describe Print a generated website API input/output contract
1082
+ doctor Alias for studio doctor
1083
+ `);
1084
+ }
1085
+ function printApisHelp() {
1086
+ console.log(`Tapi generated website API commands
1087
+
1088
+ Usage:
1089
+ tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--app APP]
1090
+
1091
+ Options:
1092
+ --api-base-url <url> Tapi API base URL
1093
+ --server <url> Alias for --api-base-url
1094
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
1095
+ --app <id> SDK app/project id; defaults to TAPI_APP_ID
987
1096
  `);
988
1097
  }
989
1098
  function printStudioHelp() {
990
- console.log(`Tapi Studio commands
991
-
992
- Usage:
993
- tapi studio install [options]
994
- tapi studio open [options]
995
- tapi studio doctor [options]
996
-
997
- Options:
998
- --channel <name> Release channel: pilot, stable, or nightly
999
- --api-base-url <url> Tapi API base URL for approval and protected downloads
1000
- --server <url> Alias for --api-base-url
1001
- --install-token <tok> Preissued Studio install token (skips browser sign-in)
1002
- --manifest <url> Exact release manifest URL for doctor only
1003
- --cache-dir <path> Installer download cache directory
1004
- --download-only Download and verify without running the installer
1005
- --silent Run the NSIS installer with /S
1006
- --exe <path> Tapi Studio executable path for open/doctor
1099
+ console.log(`Tapi Studio commands
1100
+
1101
+ Usage:
1102
+ tapi studio install [options]
1103
+ tapi studio open [options]
1104
+ tapi studio doctor [options]
1105
+
1106
+ Options:
1107
+ --channel <name> Release channel: pilot, stable, or nightly
1108
+ --api-base-url <url> Tapi API base URL for approval and protected downloads
1109
+ --server <url> Alias for --api-base-url
1110
+ --install-token <tok> Preissued Studio install token (skips browser sign-in)
1111
+ --manifest <url> Exact release manifest URL for doctor only
1112
+ --cache-dir <path> Installer download cache directory
1113
+ --download-only Download and verify without running the installer
1114
+ --silent Run the NSIS installer with /S
1115
+ --exe <path> Tapi Studio executable path for open/doctor
1007
1116
  `);
1008
1117
  }
1009
1118
  function formatError(error) {
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TapiClientOptions } from "./types";
1
+ import type { TapiClientOptions } from "./types.js";
2
2
  export declare class HttpClient {
3
3
  private readonly baseUrl;
4
4
  private readonly apiKey;
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { TapiError } from "./errors";
1
+ import { TapiError } from "./errors.js";
2
2
  export class HttpClient {
3
3
  baseUrl;
4
4
  apiKey;