@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/README.md +289 -18
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +495 -35
- package/dist/cloud-runs.d.ts +22 -0
- package/dist/cloud-runs.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/runtime.d.ts +36 -2
- package/dist/runtime.js +132 -1
- package/dist/types.d.ts +236 -1
- package/dist/website-apis.d.ts +9 -1
- package/dist/website-apis.js +61 -0
- package/package.json +36 -36
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.
|
|
105
|
+
raw.manifestUrlOverride = requireOptionValue(args, ++index, "--manifest");
|
|
78
106
|
continue;
|
|
79
107
|
}
|
|
80
108
|
if (arg.startsWith("--manifest=")) {
|
|
81
|
-
raw.
|
|
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.
|
|
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
|
|
213
|
-
|
|
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
|
-
|
|
493
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
338
494
|
channel: options.channel,
|
|
339
495
|
});
|
|
340
|
-
const manifest = await
|
|
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'>✓ 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'>⚠ 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] [--
|
|
630
|
-
tapi studio open
|
|
631
|
-
tapi studio doctor
|
|
632
|
-
tapi
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
studio
|
|
637
|
-
studio
|
|
638
|
-
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>
|
|
651
|
-
--
|
|
652
|
-
--
|
|
653
|
-
--
|
|
654
|
-
--
|
|
655
|
-
--
|
|
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
|
+
}
|