@tapi-dev/sdk 0.1.4 → 0.1.6

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/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
+ import { createServer } from "node:http";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
5
6
  import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
@@ -9,10 +10,25 @@ import { performance } from "node:perf_hooks";
9
10
  import { Readable } from "node:stream";
10
11
  import { pipeline } from "node:stream/promises";
11
12
  import { fileURLToPath } from "node:url";
13
+ import { TapiClient } from "./index";
12
14
  const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
15
+ const DEFAULT_STUDIO_API_BASE_URL = "https://determined-motivation-production.up.railway.app";
16
+ const DEFAULT_STUDIO_AUTH_HTML_URL = "https://rsarlong-1f92fd.gitlab.io/auth.html";
17
+ const DEFAULT_FIREBASE_API_KEY = "AIzaSyCDZR8lWyVQcWYfFdNZa4vuL4IWEC0h6gE";
13
18
  const DEFAULT_CHANNEL = "pilot";
14
19
  const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
20
+ const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
15
21
  const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
22
+ class StudioInstallApprovalError extends Error {
23
+ code;
24
+ status;
25
+ constructor(message, code, status) {
26
+ super(message);
27
+ this.code = code;
28
+ this.status = status;
29
+ this.name = "StudioInstallApprovalError";
30
+ }
31
+ }
16
32
  const sdkVersion = readSdkVersion();
17
33
  export async function runCli(argv = process.argv.slice(2)) {
18
34
  const [command, subcommand, ...rest] = argv;
@@ -31,6 +47,18 @@ export async function runCli(argv = process.argv.slice(2)) {
31
47
  }
32
48
  return runDoctor(parseStudioOptions([subcommand, ...rest].filter(Boolean)));
33
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
+ }
34
62
  if (command !== "studio") {
35
63
  console.error(`Unknown command: ${command}`);
36
64
  printHelp();
@@ -74,11 +102,31 @@ export function parseStudioOptions(args) {
74
102
  continue;
75
103
  }
76
104
  if (arg === "--manifest") {
77
- raw.manifestUrl = requireOptionValue(args, ++index, "--manifest");
105
+ raw.manifestUrlOverride = requireOptionValue(args, ++index, "--manifest");
78
106
  continue;
79
107
  }
80
108
  if (arg.startsWith("--manifest=")) {
81
- raw.manifestUrl = arg.slice("--manifest=".length);
109
+ raw.manifestUrlOverride = arg.slice("--manifest=".length);
110
+ continue;
111
+ }
112
+ if (arg === "--api-base-url" || arg === "--server") {
113
+ raw.apiBaseUrl = requireOptionValue(args, ++index, arg);
114
+ continue;
115
+ }
116
+ if (arg.startsWith("--api-base-url=")) {
117
+ raw.apiBaseUrl = arg.slice("--api-base-url=".length);
118
+ continue;
119
+ }
120
+ if (arg.startsWith("--server=")) {
121
+ raw.apiBaseUrl = arg.slice("--server=".length);
122
+ continue;
123
+ }
124
+ if (arg === "--install-token") {
125
+ raw.installToken = requireOptionValue(args, ++index, "--install-token");
126
+ continue;
127
+ }
128
+ if (arg.startsWith("--install-token=")) {
129
+ raw.installToken = arg.slice("--install-token=".length);
82
130
  continue;
83
131
  }
84
132
  if (arg === "--cache-dir") {
@@ -111,20 +159,106 @@ export function parseStudioOptions(args) {
111
159
  throw new Error(`Unknown option: ${arg}`);
112
160
  }
113
161
  const channel = raw.channel ?? parseChannel(envString("TAPI_STUDIO_CHANNEL") ?? DEFAULT_CHANNEL);
162
+ const apiBaseUrl = normalizeHttpUrl(raw.apiBaseUrl
163
+ ?? envString("TAPI_STUDIO_API_BASE_URL")
164
+ ?? envString("TAPI_BASE_URL")
165
+ ?? envString("TAPI_STUDIO_SERVER_URL")
166
+ ?? DEFAULT_STUDIO_API_BASE_URL, "Studio API base URL");
114
167
  const downloadsBaseUrl = normalizeHttpUrl(envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL, "TAPI_DOWNLOADS_BASE_URL");
115
- const explicitManifestUrl = raw.manifestUrl ?? envString("TAPI_STUDIO_MANIFEST_URL");
168
+ const explicitManifestUrl = raw.manifestUrlOverride ?? envString("TAPI_STUDIO_MANIFEST_URL");
116
169
  const manifestUrl = explicitManifestUrl
117
170
  ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
118
171
  : `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
119
172
  return {
120
173
  channel,
174
+ apiBaseUrl,
121
175
  manifestUrl,
176
+ manifestUrlOverride: explicitManifestUrl ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL") : undefined,
122
177
  cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
123
178
  downloadOnly: raw.downloadOnly ?? false,
124
179
  silent: raw.silent ?? false,
125
180
  exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
181
+ installToken: raw.installToken ?? envString("TAPI_STUDIO_INSTALL_TOKEN"),
182
+ };
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
+ },
126
243
  };
127
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
+ }
128
262
  export function getDefaultStudioCacheDir() {
129
263
  if (process.platform === "win32") {
130
264
  const localAppData = process.env.LOCALAPPDATA ??
@@ -209,9 +343,8 @@ export function getCliWideEventRoot() {
209
343
  return override;
210
344
  }
211
345
  if (process.platform === "win32") {
212
- const localAppData = process.env.LOCALAPPDATA ??
213
- (process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
214
- return join(localAppData, "Tapi", "logs", "events");
346
+ const programData = process.env.PROGRAMDATA?.trim() || "C:\\ProgramData";
347
+ return join(programData, "Tapi", "logs", "events");
215
348
  }
216
349
  return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "tapi", "logs", "events");
217
350
  }
@@ -332,12 +465,35 @@ async function installStudio(options) {
332
465
  arch: process.arch,
333
466
  });
334
467
  ensureWindowsHost();
468
+ if (options.manifestUrlOverride) {
469
+ throw new Error("Direct Studio manifest overrides are no longer supported for install. "
470
+ + "Use --channel with the protected install flow instead.");
471
+ }
472
+ let installToken = options.installToken?.trim();
473
+ if (installToken) {
474
+ await event.phase("auth.install_token.provided", {
475
+ apiBaseUrl: options.apiBaseUrl,
476
+ channel: options.channel,
477
+ });
478
+ }
479
+ else {
480
+ console.log("Checking Studio install approval...");
481
+ await event.phase("auth.install_token.request.start", {
482
+ apiBaseUrl: options.apiBaseUrl,
483
+ channel: options.channel,
484
+ });
485
+ installToken = await obtainStudioInstallToken(options, event);
486
+ await event.phase("auth.install_token.request.success", {
487
+ apiBaseUrl: options.apiBaseUrl,
488
+ channel: options.channel,
489
+ });
490
+ }
335
491
  console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
336
492
  await event.phase("manifest.fetch.start", {
337
- manifestUrl: options.manifestUrl,
493
+ apiBaseUrl: options.apiBaseUrl,
338
494
  channel: options.channel,
339
495
  });
340
- const manifest = await fetchStudioManifest(options.manifestUrl);
496
+ const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
341
497
  await event.phase("manifest.fetch.success", {
342
498
  manifest: manifestEventSummary(manifest),
343
499
  });
@@ -401,6 +557,49 @@ async function installStudio(options) {
401
557
  throw error;
402
558
  }
403
559
  }
560
+ async function obtainStudioInstallToken(options, event) {
561
+ const cachedCreds = await refreshCachedFirebaseCredentials();
562
+ if (cachedCreds) {
563
+ await event.phase("auth.cache.refresh.success", {
564
+ uid: cachedCreds.uid,
565
+ authCachePath: getStudioAuthCachePath(),
566
+ });
567
+ await writeStudioAuthCache(cachedCreds);
568
+ try {
569
+ const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, cachedCreds.idToken);
570
+ return issued.installToken;
571
+ }
572
+ catch (error) {
573
+ if (isApprovalTerminalError(error)) {
574
+ throw error;
575
+ }
576
+ await event.phase("auth.cache.install_token.retry_interactive", {
577
+ error: errorDetails(error),
578
+ });
579
+ }
580
+ }
581
+ else {
582
+ await event.phase("auth.cache.refresh.miss", {
583
+ authCachePath: getStudioAuthCachePath(),
584
+ });
585
+ }
586
+ const browserAuth = await browserGoogleSignIn();
587
+ await event.phase("auth.browser.callback.received", {
588
+ googleIdTokenPresent: Boolean(browserAuth.idToken),
589
+ googleAccessTokenPresent: Boolean(browserAuth.accessToken),
590
+ });
591
+ if (!browserAuth.idToken) {
592
+ throw new Error(browserAuth.error || browserAuth.detail || "Browser sign-in did not return a Google ID token.");
593
+ }
594
+ const firebaseCreds = await exchangeGoogleToFirebase(browserAuth.idToken, browserAuth.accessToken);
595
+ await writeStudioAuthCache(firebaseCreds);
596
+ await event.phase("auth.firebase.exchange.success", {
597
+ uid: firebaseCreds.uid,
598
+ authCachePath: getStudioAuthCachePath(),
599
+ });
600
+ const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, firebaseCreds.idToken);
601
+ return issued.installToken;
602
+ }
404
603
  async function openStudio(options) {
405
604
  ensureWindowsHost();
406
605
  const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
@@ -422,6 +621,7 @@ async function runDoctor(options) {
422
621
  console.log(`Tapi SDK: ${sdkVersion}`);
423
622
  console.log(`Node: ${process.version}`);
424
623
  console.log(`Platform: ${process.platform}/${process.arch}`);
624
+ console.log(`Studio API: ${options.apiBaseUrl}`);
425
625
  console.log(`Studio channel: ${options.channel}`);
426
626
  console.log(`Studio manifest: ${options.manifestUrl}`);
427
627
  console.log(`Studio cache: ${options.cacheDir}`);
@@ -438,6 +638,22 @@ async function runDoctor(options) {
438
638
  }
439
639
  return 0;
440
640
  }
641
+ async function fetchProtectedStudioManifest(apiBaseUrl, channel, installToken, fetchImpl = fetch) {
642
+ const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/studio/releases/${channel}`, {
643
+ headers: {
644
+ Accept: "application/json",
645
+ Authorization: `Bearer ${installToken}`,
646
+ },
647
+ });
648
+ const responseBody = await readJsonBody(response);
649
+ if (!response.ok) {
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}`);
654
+ }
655
+ return validateStudioManifest(responseBody);
656
+ }
441
657
  async function fetchStudioManifest(manifestUrl, fetchImpl = fetch) {
442
658
  const response = await fetchImpl(manifestUrl, {
443
659
  headers: {
@@ -449,6 +665,37 @@ async function fetchStudioManifest(manifestUrl, fetchImpl = fetch) {
449
665
  }
450
666
  return validateStudioManifest(await response.json());
451
667
  }
668
+ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, fetchImpl = fetch) {
669
+ const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/studio/install-token`, {
670
+ method: "POST",
671
+ headers: {
672
+ Accept: "application/json",
673
+ Authorization: `Bearer ${firebaseIdToken}`,
674
+ "Content-Type": "application/json",
675
+ },
676
+ body: JSON.stringify({ channel }),
677
+ });
678
+ const responseBody = await readJsonBody(response);
679
+ if (!response.ok) {
680
+ const detail = typeof responseBody?.detail === "string" ? responseBody.detail : "";
681
+ if (detail === "pending_approval" || detail === "access_pending") {
682
+ throw new StudioInstallApprovalError("Access request submitted. Check your email for approval, then rerun `npx tapi studio install`.", detail, response.status);
683
+ }
684
+ if (detail === "access_rejected") {
685
+ throw new StudioInstallApprovalError("Studio install access was rejected. Contact the Tapi admin if this is unexpected.", detail, response.status);
686
+ }
687
+ throw new StudioInstallApprovalError(detail || `Failed to request Studio install token: HTTP ${response.status}`, detail || "install_token_request_failed", response.status);
688
+ }
689
+ const payload = responseBody;
690
+ const installToken = typeof payload?.installToken === "string" ? payload.installToken.trim() : "";
691
+ if (!installToken) {
692
+ throw new Error("Studio install token response did not include installToken.");
693
+ }
694
+ return {
695
+ installToken,
696
+ expiresAt: typeof payload?.expiresAt === "string" ? payload.expiresAt : undefined,
697
+ };
698
+ }
452
699
  async function downloadAndVerify(url, destination, expectedSha256) {
453
700
  const partialPath = `${destination}.partial`;
454
701
  await downloadFile(url, partialPath);
@@ -470,6 +717,198 @@ async function downloadFile(url, destination) {
470
717
  }
471
718
  await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
472
719
  }
720
+ async function browserGoogleSignIn(timeoutMs = DEFAULT_INSTALL_AUTH_TIMEOUT_MS) {
721
+ ensureWindowsHost();
722
+ return new Promise((resolvePromise, rejectPromise) => {
723
+ let timeoutHandle;
724
+ const cleanup = () => {
725
+ if (timeoutHandle) {
726
+ clearTimeout(timeoutHandle);
727
+ timeoutHandle = undefined;
728
+ }
729
+ try {
730
+ server.close();
731
+ }
732
+ catch {
733
+ // ignore close races
734
+ }
735
+ };
736
+ const server = createServer((req, res) => {
737
+ const requestUrl = new URL(req.url || "/", "http://127.0.0.1");
738
+ const idToken = requestUrl.searchParams.get("idToken") || "";
739
+ const accessToken = requestUrl.searchParams.get("accessToken") || "";
740
+ const error = requestUrl.searchParams.get("error") || "";
741
+ const detail = requestUrl.searchParams.get("detail") || "";
742
+ const ok = Boolean(idToken);
743
+ res.statusCode = 200;
744
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
745
+ res.end(renderBrowserSignInResponseHtml(ok));
746
+ cleanup();
747
+ if (!ok) {
748
+ rejectPromise(new Error(error || detail || "Browser sign-in did not return Google credentials."));
749
+ return;
750
+ }
751
+ resolvePromise({
752
+ accessToken: accessToken || undefined,
753
+ detail: detail || undefined,
754
+ idToken,
755
+ });
756
+ });
757
+ timeoutHandle = setTimeout(() => {
758
+ cleanup();
759
+ rejectPromise(new Error("Browser sign-in timed out or was cancelled."));
760
+ }, timeoutMs);
761
+ server.once("error", (error) => {
762
+ cleanup();
763
+ rejectPromise(error);
764
+ });
765
+ server.listen(0, "127.0.0.1", () => {
766
+ const address = server.address();
767
+ const port = typeof address === "object" && address ? address.port : 0;
768
+ if (!port) {
769
+ cleanup();
770
+ rejectPromise(new Error("Could not start local callback server for Studio sign-in."));
771
+ return;
772
+ }
773
+ try {
774
+ openBrowser(`${getStudioAuthHtmlUrl()}?port=${port}`);
775
+ }
776
+ catch (error) {
777
+ cleanup();
778
+ rejectPromise(error);
779
+ }
780
+ });
781
+ });
782
+ }
783
+ async function exchangeGoogleToFirebase(googleIdToken, googleAccessToken, fetchImpl = fetch) {
784
+ const postBody = [
785
+ `id_token=${encodeURIComponent(googleIdToken)}`,
786
+ "providerId=google.com",
787
+ googleAccessToken ? `access_token=${encodeURIComponent(googleAccessToken)}` : "",
788
+ ].filter(Boolean).join("&");
789
+ const response = await fetchImpl(`https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp?key=${getFirebaseApiKey()}`, {
790
+ method: "POST",
791
+ headers: {
792
+ Accept: "application/json",
793
+ "Content-Type": "application/json",
794
+ },
795
+ body: JSON.stringify({
796
+ postBody,
797
+ requestUri: "http://127.0.0.1",
798
+ returnSecureToken: true,
799
+ returnIdpCredential: true,
800
+ }),
801
+ });
802
+ const responseBody = await readJsonBody(response);
803
+ if (!response.ok) {
804
+ throw new Error(`Firebase exchange failed: HTTP ${response.status}`);
805
+ }
806
+ const uid = typeof responseBody?.localId === "string" ? responseBody.localId.trim() : "";
807
+ const idToken = typeof responseBody?.idToken === "string" ? responseBody.idToken.trim() : "";
808
+ const refreshToken = typeof responseBody?.refreshToken === "string" ? responseBody.refreshToken.trim() : "";
809
+ if (!uid || !idToken || !refreshToken) {
810
+ throw new Error("Firebase exchange response is missing uid, idToken, or refreshToken.");
811
+ }
812
+ return { uid, idToken, refreshToken };
813
+ }
814
+ async function refreshCachedFirebaseCredentials(fetchImpl = fetch) {
815
+ const cache = readStudioAuthCache();
816
+ if (!cache?.refreshToken) {
817
+ return null;
818
+ }
819
+ const response = await fetchImpl(`https://securetoken.googleapis.com/v1/token?key=${getFirebaseApiKey()}`, {
820
+ method: "POST",
821
+ headers: {
822
+ Accept: "application/json",
823
+ "Content-Type": "application/x-www-form-urlencoded",
824
+ },
825
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(cache.refreshToken)}`,
826
+ });
827
+ const responseBody = await readJsonBody(response);
828
+ if (!response.ok) {
829
+ return null;
830
+ }
831
+ const uid = typeof responseBody?.user_id === "string" ? responseBody.user_id.trim() : "";
832
+ const idToken = typeof responseBody?.id_token === "string" ? responseBody.id_token.trim() : "";
833
+ const refreshToken = typeof responseBody?.refresh_token === "string" ? responseBody.refresh_token.trim() : "";
834
+ if (!uid || !idToken || !refreshToken) {
835
+ return null;
836
+ }
837
+ return { uid, idToken, refreshToken };
838
+ }
839
+ function readStudioAuthCache() {
840
+ const cachePath = getStudioAuthCachePath();
841
+ if (!existsSync(cachePath)) {
842
+ return null;
843
+ }
844
+ try {
845
+ const payload = JSON.parse(readFileSync(cachePath, "utf8"));
846
+ const uid = typeof payload.uid === "string" ? payload.uid.trim() : "";
847
+ const idToken = typeof payload.id_token === "string" ? payload.id_token.trim() : "";
848
+ const refreshToken = typeof payload.refresh_token === "string" ? payload.refresh_token.trim() : "";
849
+ if (!uid || !refreshToken) {
850
+ return null;
851
+ }
852
+ return { uid, idToken, refreshToken };
853
+ }
854
+ catch {
855
+ return null;
856
+ }
857
+ }
858
+ async function writeStudioAuthCache(credentials) {
859
+ await mkdir(getDefaultTapiDataDir(), { recursive: true });
860
+ await writeFile(getStudioAuthCachePath(), `${JSON.stringify({
861
+ uid: credentials.uid,
862
+ refresh_token: credentials.refreshToken,
863
+ id_token: credentials.idToken,
864
+ }, null, 2)}\n`, "utf8");
865
+ }
866
+ function getStudioAuthCachePath() {
867
+ return join(getDefaultTapiDataDir(), "auth.json");
868
+ }
869
+ function getDefaultTapiDataDir() {
870
+ if (process.platform === "win32") {
871
+ const localAppData = process.env.LOCALAPPDATA
872
+ ?? (process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
873
+ return join(localAppData, "Tapi");
874
+ }
875
+ return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "tapi");
876
+ }
877
+ function getStudioAuthHtmlUrl() {
878
+ return normalizeHttpUrl(envString("TAPI_STUDIO_AUTH_HTML_URL") ?? DEFAULT_STUDIO_AUTH_HTML_URL, "TAPI_STUDIO_AUTH_HTML_URL");
879
+ }
880
+ function getFirebaseApiKey() {
881
+ return (envString("TAPI_FIREBASE_API_KEY") ?? DEFAULT_FIREBASE_API_KEY).trim();
882
+ }
883
+ function openBrowser(url) {
884
+ const child = spawn("cmd", ["/c", "start", "", url], {
885
+ detached: true,
886
+ stdio: "ignore",
887
+ windowsHide: true,
888
+ });
889
+ child.unref();
890
+ }
891
+ async function readJsonBody(response) {
892
+ const text = await response.text();
893
+ if (!text.trim()) {
894
+ return null;
895
+ }
896
+ try {
897
+ return JSON.parse(text);
898
+ }
899
+ catch {
900
+ return null;
901
+ }
902
+ }
903
+ function isApprovalTerminalError(error) {
904
+ return error instanceof StudioInstallApprovalError
905
+ && (error.code === "pending_approval" || error.code === "access_pending" || error.code === "access_rejected");
906
+ }
907
+ function renderBrowserSignInResponseHtml(success) {
908
+ return success
909
+ ? "<!DOCTYPE html><html><body style='font-family:system-ui;text-align:center;padding:80px'><h2 style='color:#22c55e'>&#10003; Signed in!</h2><p>Return to your terminal.</p><script>setTimeout(()=>window.close(),1600)</script></body></html>"
910
+ : "<!DOCTYPE html><html><body style='font-family:system-ui;text-align:center;padding:80px'><h2 style='color:#ef4444'>&#9888; Sign-in failed</h2><p>Return to your terminal for details.</p></body></html>";
911
+ }
473
912
  async function hasVerifiedCachedInstaller(path, expectedSha256) {
474
913
  if (!existsSync(path)) {
475
914
  return false;
@@ -518,11 +957,14 @@ function cachedInstallerName(manifest) {
518
957
  function installEventOptions(options) {
519
958
  return {
520
959
  channel: options.channel,
960
+ apiBaseUrl: options.apiBaseUrl,
521
961
  manifestUrl: options.manifestUrl,
962
+ manifestUrlOverride: options.manifestUrlOverride,
522
963
  cacheDir: options.cacheDir,
523
964
  downloadOnly: options.downloadOnly,
524
965
  silent: options.silent,
525
966
  exePath: options.exePath,
967
+ installTokenProvided: Boolean(options.installToken),
526
968
  };
527
969
  }
528
970
  function manifestEventSummary(manifest) {
@@ -623,36 +1065,54 @@ function readSdkVersion() {
623
1065
  return packageJson.version;
624
1066
  }
625
1067
  function printHelp() {
626
- console.log(`Tapi CLI
627
-
628
- Usage:
629
- tapi studio install [--channel pilot] [--manifest URL]
630
- tapi studio open
631
- tapi studio doctor
632
- tapi doctor
633
-
634
- Commands:
635
- studio install Download, verify, and run the Tapi Studio installer
636
- studio open Open an installed Tapi Studio desktop app
637
- studio doctor Check local SDK and Studio release configuration
638
- 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
639
1096
  `);
640
1097
  }
641
1098
  function printStudioHelp() {
642
- console.log(`Tapi Studio commands
643
-
644
- Usage:
645
- tapi studio install [options]
646
- tapi studio open [options]
647
- tapi studio doctor [options]
648
-
649
- Options:
650
- --channel <name> Release channel: pilot, stable, or nightly
651
- --manifest <url> Exact release manifest URL
652
- --cache-dir <path> Installer download cache directory
653
- --download-only Download and verify without running the installer
654
- --silent Run the NSIS installer with /S
655
- --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
656
1116
  `);
657
1117
  }
658
1118
  function formatError(error) {
@@ -0,0 +1,22 @@
1
+ import type { HttpClient } from "./client";
2
+ import type { CloudBatchRun, CloudBalance, CloudCreditCheckout, CloudCreditCheckoutRequest, CloudRunEvent, CloudRunResults, CloudRunUser } from "./types";
3
+ export declare class CloudRunsResource {
4
+ private readonly http;
5
+ constructor(http: HttpClient);
6
+ get(runId: string): Promise<CloudBatchRun>;
7
+ events(runId: string): Promise<{
8
+ events: CloudRunEvent[];
9
+ }>;
10
+ results(runId: string): Promise<CloudRunResults>;
11
+ cancel(runId: string): Promise<CloudBatchRun>;
12
+ balance(user?: CloudRunUser): Promise<CloudBalance>;
13
+ checkout(request: CloudCreditCheckoutRequest): Promise<CloudCreditCheckout>;
14
+ wait(runId: string, options?: {
15
+ intervalMs?: number;
16
+ timeoutMs?: number;
17
+ }): Promise<CloudBatchRun>;
18
+ stream(runId: string, options?: {
19
+ intervalMs?: number;
20
+ timeoutMs?: number;
21
+ }): AsyncGenerator<CloudRunEvent, void, unknown>;
22
+ }