@typeship-ax/cli 0.6.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +31 -0
- package/README.md +26 -9
- package/api.json +6861 -3083
- package/api.md +508 -277
- package/dist/api-identity.d.ts +40 -0
- package/dist/api-identity.d.ts.map +1 -0
- package/dist/api-identity.js +128 -0
- package/dist/auth-profiles.d.ts +30 -0
- package/dist/auth-profiles.d.ts.map +1 -0
- package/dist/auth-profiles.js +138 -0
- package/dist/cli-agent.d.ts +9 -1
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +26 -9
- package/dist/cli.js +600 -582
- package/dist/console-login-check.d.ts +21 -0
- package/dist/console-login-check.d.ts.map +1 -0
- package/dist/console-login-check.js +107 -0
- package/dist/console-login-contract.d.ts +45 -0
- package/dist/console-login-contract.d.ts.map +1 -0
- package/dist/console-login-contract.js +40 -0
- package/dist/core/http.d.ts +21 -92
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +143 -221
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +6 -34
- package/dist/credential-storage.d.ts +24 -0
- package/dist/credential-storage.d.ts.map +1 -0
- package/dist/credential-storage.js +207 -0
- package/dist/dates.d.ts +0 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +0 -1
- package/dist/docs.d.ts +36 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +258 -0
- package/dist/errors.d.ts +42 -34
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +30 -20
- package/dist/index.d.ts +27 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -14
- package/dist/named-credentials.d.ts +21 -0
- package/dist/named-credentials.d.ts.map +1 -0
- package/dist/named-credentials.js +86 -0
- package/dist/oauth-login.d.ts +39 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +171 -0
- package/dist/oauth-request.d.ts +21 -0
- package/dist/oauth-request.d.ts.map +1 -0
- package/dist/oauth-request.js +119 -0
- package/dist/oauth-session.d.ts +106 -0
- package/dist/oauth-session.d.ts.map +1 -0
- package/dist/oauth-session.js +244 -0
- package/dist/ops.d.ts +18 -0
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +31 -17
- package/dist/polling-login.d.ts +57 -0
- package/dist/polling-login.d.ts.map +1 -0
- package/dist/polling-login.js +204 -0
- package/dist/resources/account.d.ts +4 -4
- package/dist/resources/account.d.ts.map +1 -1
- package/dist/resources/account.js +1 -0
- package/dist/resources/api-keys.d.ts +13 -8
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +5 -1
- package/dist/resources/definition-revisions.d.ts +58 -0
- package/dist/resources/definition-revisions.d.ts.map +1 -0
- package/dist/resources/definition-revisions.js +114 -0
- package/dist/resources/definitions.d.ts +35 -0
- package/dist/resources/definitions.d.ts.map +1 -0
- package/dist/resources/definitions.js +60 -0
- package/dist/resources/generate.d.ts +18 -7
- package/dist/resources/generate.d.ts.map +1 -1
- package/dist/resources/generate.js +13 -5
- package/dist/resources/generations.d.ts +6 -6
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +3 -1
- package/dist/resources/projects.d.ts +111 -35
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +125 -15
- package/dist/resources/targets.d.ts +97 -0
- package/dist/resources/targets.d.ts.map +1 -0
- package/dist/resources/targets.js +197 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +135 -62
- package/dist/types.d.ts +2072 -267
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +20 -3
- package/package.json +2 -1
- package/src/api-identity.ts +98 -0
- package/src/auth-profiles.ts +114 -0
- package/src/cli-agent.ts +28 -11
- package/src/cli.ts +528 -560
- package/src/console-login-check.ts +88 -0
- package/src/console-login-contract.ts +65 -0
- package/src/core/http.ts +156 -305
- package/src/core/pagination.ts +6 -30
- package/src/credential-storage.ts +183 -0
- package/src/dates.ts +0 -1
- package/src/docs.ts +239 -0
- package/src/errors.ts +52 -41
- package/src/index.ts +49 -14
- package/src/named-credentials.ts +74 -0
- package/src/oauth-login.ts +184 -0
- package/src/oauth-request.ts +90 -0
- package/src/oauth-session.ts +258 -0
- package/src/ops.ts +56 -17
- package/src/polling-login.ts +165 -0
- package/src/resources/account.ts +6 -3
- package/src/resources/api-keys.ts +27 -7
- package/src/resources/definition-revisions.ts +207 -0
- package/src/resources/definitions.ts +122 -0
- package/src/resources/generate.ts +29 -6
- package/src/resources/generations.ts +9 -4
- package/src/resources/projects.ts +274 -41
- package/src/resources/targets.ts +378 -0
- package/src/schemas.ts +135 -62
- package/src/types.ts +2273 -322
- package/dist/resources/spec-revisions.d.ts +0 -47
- package/dist/resources/spec-revisions.d.ts.map +0 -1
- package/dist/resources/spec-revisions.js +0 -90
- package/src/resources/spec-revisions.ts +0 -150
package/dist/cli.js
CHANGED
|
@@ -5,33 +5,59 @@
|
|
|
5
5
|
// Prints raw JSON to stdout; errors as JSON on stderr.
|
|
6
6
|
// Exit codes: 0 success, 1 API/transport error, 2 usage error.
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { oauthBrowserLogin } from "./oauth-login.js";
|
|
9
|
+
import { oauthDeviceLogin, customBrowserApproval, loginEndpoint } from "./polling-login.js";
|
|
10
|
+
import { oauthStatusRequest } from "./oauth-request.js";
|
|
11
|
+
import { checkConsoleBrowserLogin } from "./console-login-check.js";
|
|
12
|
+
import { assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, oauthSessionToken, sessionBinding } from "./oauth-session.js";
|
|
13
|
+
import { createCredentialStore } from "./credential-storage.js";
|
|
14
|
+
import { identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity } from "./api-identity.js";
|
|
15
|
+
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability } from "./named-credentials.js";
|
|
16
|
+
import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig } from "./auth-profiles.js";
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
10
19
|
import { homedir, hostname } from "node:os";
|
|
11
20
|
import { basename, join } from "node:path";
|
|
12
21
|
import { fileURLToPath } from "node:url";
|
|
13
22
|
import { TypeshipClient, formatDebugEvent } from "./index.js";
|
|
14
|
-
import { GLOBALS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
|
|
23
|
+
import { GLOBALS, OMITTED_OPS, OPS, buildArgs, findOp, missingRequired } from "./ops.js";
|
|
15
24
|
import { MCP_CLIENTS, agentGuide, agentBlock, agentInstructionsFile, agentMode, bundleProperty, claimProperty, classifyApiError, collectionProperty, detectHarness, envelope, exitCodeFor, findMcpClient, installSkills, mcpConfigured, pendingClaims, recordClaim, summarizeDoctor, upsertAgentBlock, writeBundle, writeMcpConfig, } from "./cli-agent.js";
|
|
16
25
|
import { relativeDate } from "./dates.js";
|
|
26
|
+
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
17
27
|
const BIN = "typeship";
|
|
18
28
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
29
|
+
const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
|
|
19
30
|
const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
|
|
20
31
|
const BASIC = null;
|
|
32
|
+
/** Operations omitted from the generated package by its plan cap. */
|
|
21
33
|
const EXCLUDED_OPS = 0;
|
|
22
|
-
|
|
23
|
-
const
|
|
34
|
+
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
35
|
+
const MCP_EXCLUDED_OPS = 0;
|
|
36
|
+
const VERSION = "0.9.1";
|
|
37
|
+
const API_VERSION = "1.0.0";
|
|
24
38
|
const SPEC_FORMAT = "openapi";
|
|
39
|
+
const IDENTITY_POLICY = {};
|
|
40
|
+
let LOGIN_IDENTITY;
|
|
25
41
|
const WHOAMI = { "resource": "account", "method": "retrieve" };
|
|
26
42
|
const ENVIRONMENTS = {};
|
|
27
43
|
const HAS_MCP = false;
|
|
28
44
|
const PKG_NAME = "@typeship-ax/cli";
|
|
29
45
|
const UPDATE_NOTICE = false;
|
|
30
|
-
const API_DESCRIPTION = "
|
|
46
|
+
const API_DESCRIPTION = "Resolve an OpenAPI or GraphQL Definition, diagnose it, and keep every\nselected SDK, CLI, and MCP Target current.\n\nEvery operation but one requires a bearer credential: an organization\nAPI key from the console, or an OAuth access token carrying the operation's\nread, generate, or write capability and the organization selected during\nconsent. OAuth grants cannot switch organizations after consent. A browser\nsession is not a credential for this API. The exception is POST /generate,\nwhich works anonymously with the free plan's limits.\n";
|
|
31
47
|
const DOCS_URL_DEFAULT = "https://typeship.dev";
|
|
48
|
+
const DOCS_INDEX_URL_DEFAULT = null;
|
|
32
49
|
const RELAY = null;
|
|
33
50
|
const SUPPORT_URL = null;
|
|
34
51
|
const OAUTH_TOKEN_URL = null;
|
|
52
|
+
const OAUTH_ISSUER = null;
|
|
53
|
+
const OAUTH_DISCOVERY_URL = undefined;
|
|
54
|
+
const OAUTH_DISCOVERY_URLS = [];
|
|
55
|
+
const OAUTH_DEVICE_URL = null;
|
|
56
|
+
const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
|
|
57
|
+
const OAUTH_LOGIN_METHOD = "device";
|
|
58
|
+
const OAUTH_REDIRECT_URI = undefined;
|
|
59
|
+
const OAUTH_ORGANIZATION_PARAMETER = undefined;
|
|
60
|
+
const OAUTH_AUTHORIZATION_URL = undefined;
|
|
35
61
|
const OAUTH_CLIENT_ID = null;
|
|
36
62
|
const OAUTH_SCOPES = [];
|
|
37
63
|
const OAUTH_TOKEN_PARAMS = {};
|
|
@@ -48,11 +74,12 @@ const API_TITLE = "typeship";
|
|
|
48
74
|
* is known. */
|
|
49
75
|
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
|
|
50
76
|
const BUILTIN_BOOLEAN_FLAGS = {
|
|
51
|
-
login: ["with-token", "no-browser"],
|
|
77
|
+
login: ["with-token", "no-browser", "device"],
|
|
78
|
+
logout: ["local"],
|
|
52
79
|
upgrade: ["check"],
|
|
53
80
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
54
81
|
docs: ["web", "schema"],
|
|
55
|
-
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md"],
|
|
82
|
+
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
56
83
|
auth: ["live"],
|
|
57
84
|
doctor: [],
|
|
58
85
|
};
|
|
@@ -271,7 +298,7 @@ function humanError(body, code) {
|
|
|
271
298
|
const firstWords = (issue?.message ?? "").slice(0, 40);
|
|
272
299
|
if (compact && compact !== "{}" && !(firstWords && compact.includes(firstWords)))
|
|
273
300
|
lines.push(" " + paintErr("dim", "API said: " + (compact.length > 300 ? compact.slice(0, 297) + "…" : compact)));
|
|
274
|
-
const requestId = detail.body?.request_id ?? detail.body?.requestId;
|
|
301
|
+
const requestId = detail.request_id ?? detail.body?.request_id ?? detail.body?.requestId;
|
|
275
302
|
if (typeof requestId === "string")
|
|
276
303
|
lines.push(" " + paintErr("dim", "request id: " + requestId));
|
|
277
304
|
}
|
|
@@ -290,7 +317,7 @@ function fail(code, message, extra, nextSteps) {
|
|
|
290
317
|
: /^(Missing required|Expected \d+ argument)/.test(message) ? "MISSING_ARGUMENT"
|
|
291
318
|
: "INVALID_USAGE";
|
|
292
319
|
return failWith({
|
|
293
|
-
code: code === 2 ? usageCode : "
|
|
320
|
+
code: code === 2 ? usageCode : "CALL_FAILED",
|
|
294
321
|
message,
|
|
295
322
|
...(extra !== undefined ? { detail: extra } : {}),
|
|
296
323
|
nextSteps: nextSteps ?? (code === 2 ? ["Run '" + USAGE_HINT + "' for the flags this command takes."] : []),
|
|
@@ -300,24 +327,92 @@ function fail(code, message, extra, nextSteps) {
|
|
|
300
327
|
function failApi(error, hadCredential) {
|
|
301
328
|
return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
|
|
302
329
|
}
|
|
303
|
-
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// credentials — written by `login`, cleared by `logout`
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
function configRoot() {
|
|
304
334
|
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
305
335
|
return join(base, BIN);
|
|
306
336
|
}
|
|
337
|
+
let PROFILE;
|
|
338
|
+
function configDir() { return PROFILE.directory; }
|
|
307
339
|
function credsPath() {
|
|
308
|
-
return
|
|
340
|
+
return credentialStore().path;
|
|
309
341
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
342
|
+
function credentialStore() { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
343
|
+
function readCreds() { return credentialStore().read(); }
|
|
344
|
+
function sessionConfiguration(baseUrl, config = readConfig()) {
|
|
345
|
+
return {
|
|
346
|
+
apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name,
|
|
347
|
+
identity: identityConfiguration(),
|
|
348
|
+
clientId: process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID,
|
|
349
|
+
issuer: OAUTH_ISSUER, discoveryUrl: OAUTH_DISCOVERY_URL, tokenUrl: OAUTH_TOKEN_URL,
|
|
350
|
+
authorizationUrl: OAUTH_AUTHORIZATION_URL, scopes: OAUTH_SCOPES,
|
|
351
|
+
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
352
|
+
organizationParameter: OAUTH_ORGANIZATION_PARAMETER,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function identityConfiguration() {
|
|
356
|
+
return Object.keys(IDENTITY_POLICY).length && WHOAMI ? { operation: WHOAMI.resource + "." + WHOAMI.method, fields: IDENTITY_POLICY } : undefined;
|
|
357
|
+
}
|
|
358
|
+
function expectedLoginIdentity(flags) {
|
|
359
|
+
const expected = {};
|
|
360
|
+
for (const kind of ["subject", "account", "organization"]) {
|
|
361
|
+
const value = flags.get(kind);
|
|
362
|
+
if (value === undefined)
|
|
363
|
+
continue;
|
|
364
|
+
if (typeof value !== "string" || !value || !IDENTITY_POLICY[kind])
|
|
365
|
+
fail(2, "--" + kind + " requires an expected ID and a configured identity field for that ID.");
|
|
366
|
+
expected[kind] = value;
|
|
313
367
|
}
|
|
314
|
-
|
|
315
|
-
|
|
368
|
+
const selection = requestedLoginOrganization(flags);
|
|
369
|
+
if (selection) {
|
|
370
|
+
if (expected.organization !== undefined && expected.organization !== selection.id)
|
|
371
|
+
fail(2, "--organization must match --login-organization when both are provided.");
|
|
372
|
+
expected.organization = selection.id;
|
|
316
373
|
}
|
|
374
|
+
return Object.keys(expected).length ? expected : undefined;
|
|
317
375
|
}
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
376
|
+
function requestedLoginOrganization(flags) {
|
|
377
|
+
const id = flags.get("login-organization");
|
|
378
|
+
if (id === undefined)
|
|
379
|
+
return;
|
|
380
|
+
if (!OAUTH_ORGANIZATION_PARAMETER || !IDENTITY_POLICY.organization)
|
|
381
|
+
fail(2, "--login-organization requires auth.oauth_organization_parameter and identity_organization configured by the API owner.");
|
|
382
|
+
if (typeof id !== "string" || !id || id.length > 512 || /\s|[\u0000-\u001F\u007F]/.test(id))
|
|
383
|
+
fail(2, "--login-organization requires a nonempty provider organization ID without whitespace.");
|
|
384
|
+
return { parameter: OAUTH_ORGANIZATION_PARAMETER, id: id };
|
|
385
|
+
}
|
|
386
|
+
function loginIdentityReport() { return LOGIN_IDENTITY ? { verified_identity: LOGIN_IDENTITY.values, identity_checked_at: LOGIN_IDENTITY.checkedAt } : {}; }
|
|
387
|
+
/** Each login replaces this profile's credentials and retains its API destination. */
|
|
388
|
+
async function saveLoginCredentials(credentials, flags, destination, onIdentityFailure) {
|
|
389
|
+
const config = readConfig();
|
|
390
|
+
const apiBaseUrl = destination?.apiBaseUrl ?? resolveBaseUrl(flags, config);
|
|
391
|
+
if (!apiBaseUrl)
|
|
392
|
+
fail(2, "Set the API base URL before logging in.");
|
|
393
|
+
const url = new URL(apiBaseUrl);
|
|
394
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
395
|
+
fail(2, "The API base URL must be HTTP or HTTPS without credentials, query, or fragment.");
|
|
396
|
+
const bound = destination ? { apiBaseUrl: destination.apiBaseUrl, environment: destination.environment, profile: destination.profile } : { apiBaseUrl: url.href, environment: config.environment, profile: PROFILE.name };
|
|
397
|
+
const next = { ...credentials, destination: bound };
|
|
398
|
+
const identity = identityConfiguration();
|
|
399
|
+
if (identity && WHOAMI) {
|
|
400
|
+
const op = OPS.find((op) => op.resource === WHOAMI.resource && op.method === WHOAMI.method);
|
|
401
|
+
if (!op)
|
|
402
|
+
fail(2, "The identity operation is unavailable. Regenerate this product with the configured identity read.");
|
|
403
|
+
const transport = identityFetch(bound.apiBaseUrl);
|
|
404
|
+
const verified = await verifyApiIdentity(async (anonymous) => {
|
|
405
|
+
const client = anonymous ? new TypeshipClient({ baseUrl: bound.apiBaseUrl, fetch: transport, maxRetries: 0, timeoutMs: 10_000 }) : await makeClient(flags, op, next, true);
|
|
406
|
+
return identityResult(client, op);
|
|
407
|
+
}, identity.fields, expectedLoginIdentity(flags)).catch(async (error) => {
|
|
408
|
+
await onIdentityFailure?.();
|
|
409
|
+
throw error;
|
|
410
|
+
});
|
|
411
|
+
next.identity = { ...verified, binding: credentialIdentityBinding(next, identity) };
|
|
412
|
+
LOGIN_IDENTITY = verified;
|
|
413
|
+
}
|
|
414
|
+
await credentialStore().update(() => next);
|
|
415
|
+
await updateProfileConfig(configDir(), (current) => ({ ...current, baseUrl: bound.apiBaseUrl }));
|
|
321
416
|
}
|
|
322
417
|
function readStdin() {
|
|
323
418
|
return new Promise((resolve) => {
|
|
@@ -379,130 +474,48 @@ function promptHidden(promptText) {
|
|
|
379
474
|
stdin.on("data", onData);
|
|
380
475
|
});
|
|
381
476
|
}
|
|
382
|
-
async function
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
});
|
|
388
|
-
let body = null;
|
|
477
|
+
async function withLoginCancellation(work) {
|
|
478
|
+
const controller = new AbortController();
|
|
479
|
+
const cancel = () => controller.abort();
|
|
480
|
+
process.once("SIGINT", cancel);
|
|
481
|
+
process.once("SIGTERM", cancel);
|
|
389
482
|
try {
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
async function deviceLogin(clientId,
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
fail(1, "The authorization server does not advertise a device flow. Run '" + BIN + " login' with a pasted credential instead.");
|
|
420
|
-
}
|
|
421
|
-
const start = await oauthForm(deviceEndpoint, {
|
|
422
|
-
client_id: clientId,
|
|
423
|
-
...(OAUTH_SCOPES.length > 0 ? { scope: OAUTH_SCOPES.join(" ") } : {}),
|
|
424
|
-
...OAUTH_TOKEN_PARAMS,
|
|
425
|
-
});
|
|
426
|
-
const startBody = start.body;
|
|
427
|
-
if (start.status !== 200 || !startBody?.device_code) {
|
|
428
|
-
fail(1, "Device authorization failed (HTTP " + start.status + ").", start.body);
|
|
429
|
-
}
|
|
430
|
-
const uri = startBody.verification_uri_complete ?? startBody.verification_uri;
|
|
431
|
-
process.stderr.write("Open " + paintErr("cyan", String(uri)) + " and enter code: " + paintErr("bold", String(startBody.user_code)) + "\n");
|
|
432
|
-
// Under an agent the same facts also go out as one JSON line (stderr,
|
|
433
|
-
// so stdout stays the single result document), for the agent to hand
|
|
434
|
-
// the URL and code to the user while this polls.
|
|
435
|
-
if (agent)
|
|
436
|
-
process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: uri, user_code: startBody.user_code, expires_in: startBody.expires_in ?? 900 }) + "\n");
|
|
437
|
-
let intervalMs = (startBody.interval ?? 5) * 1000;
|
|
438
|
-
const deadline = Date.now() + (startBody.expires_in ?? 900) * 1000;
|
|
439
|
-
while (Date.now() < deadline) {
|
|
440
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
441
|
-
const poll = await oauthForm(tokenEndpoint, {
|
|
442
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
443
|
-
device_code: startBody.device_code,
|
|
444
|
-
client_id: clientId,
|
|
445
|
-
});
|
|
446
|
-
const tokenBody = poll.body;
|
|
447
|
-
if (poll.status === 200 && tokenBody?.access_token) {
|
|
448
|
-
writeCreds({
|
|
449
|
-
...(readCreds() ?? {}),
|
|
450
|
-
oauth: {
|
|
451
|
-
accessToken: tokenBody.access_token,
|
|
452
|
-
refreshToken: tokenBody.refresh_token,
|
|
453
|
-
expiresAt: tokenBody.expires_in ? Date.now() + tokenBody.expires_in * 1000 : undefined,
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
|
-
process.stderr.write(paintErr("green", "Logged in.") + "\n");
|
|
457
|
-
out({ ok: true, method: "device", credentials: credsPath() });
|
|
458
|
-
await flushExit(0);
|
|
459
|
-
}
|
|
460
|
-
const errorCode = tokenBody?.error;
|
|
461
|
-
if (errorCode === "authorization_pending")
|
|
462
|
-
continue;
|
|
463
|
-
if (errorCode === "slow_down") {
|
|
464
|
-
intervalMs += 5000;
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
fail(1, "Device login failed: " + (errorCode ?? "HTTP " + poll.status), poll.body);
|
|
468
|
-
}
|
|
469
|
-
fail(1, "Device login timed out before the code was entered.");
|
|
470
|
-
}
|
|
471
|
-
/** Stored OAuth access token, refreshed through the token URL when expired.
|
|
472
|
-
* A failed refresh returns the stale token; the API's 401 explains better
|
|
473
|
-
* than a local guess. */
|
|
474
|
-
async function refreshedOauthToken(stored) {
|
|
475
|
-
const oauth = stored.oauth;
|
|
476
|
-
if (!oauth)
|
|
477
|
-
return undefined;
|
|
478
|
-
const expired = oauth.expiresAt !== undefined && Date.now() > oauth.expiresAt - 60_000;
|
|
479
|
-
if (!expired || !oauth.refreshToken || !OAUTH_TOKEN_URL)
|
|
480
|
-
return oauth.accessToken;
|
|
481
|
-
const clientId = process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
482
|
-
const result = await oauthForm(OAUTH_TOKEN_URL, {
|
|
483
|
-
grant_type: "refresh_token",
|
|
484
|
-
refresh_token: oauth.refreshToken,
|
|
485
|
-
...(clientId ? { client_id: clientId } : {}),
|
|
486
|
-
});
|
|
487
|
-
const body = result.body;
|
|
488
|
-
if (result.status === 200 && body?.access_token) {
|
|
489
|
-
const next = {
|
|
490
|
-
accessToken: body.access_token,
|
|
491
|
-
refreshToken: body.refresh_token ?? oauth.refreshToken,
|
|
492
|
-
expiresAt: body.expires_in ? Date.now() + body.expires_in * 1000 : undefined,
|
|
493
|
-
};
|
|
494
|
-
writeCreds({ ...stored, oauth: next });
|
|
495
|
-
return next.accessToken;
|
|
496
|
-
}
|
|
497
|
-
return oauth.accessToken;
|
|
483
|
+
return await work(controller.signal);
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
process.off("SIGINT", cancel);
|
|
487
|
+
process.off("SIGTERM", cancel);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function deviceLogin(clientId, parsed) {
|
|
491
|
+
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
492
|
+
if (!apiBaseUrl)
|
|
493
|
+
fail(2, "Set the API base URL before logging in.");
|
|
494
|
+
const loginConfiguration = sessionConfiguration(apiBaseUrl);
|
|
495
|
+
await credentialStore().prepare();
|
|
496
|
+
const session = await withLoginCancellation((signal) => oauthDeviceLogin({
|
|
497
|
+
clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
|
|
498
|
+
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: OAUTH_SCOPES,
|
|
499
|
+
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
500
|
+
}, { signal, authorize({ verificationUri, userCode, expiresIn }) {
|
|
501
|
+
process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
|
|
502
|
+
if (isAgentMode(parsed))
|
|
503
|
+
process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: verificationUri, user_code: userCode, expires_in: expiresIn }) + "\n");
|
|
504
|
+
} }));
|
|
505
|
+
await saveLoginCredentials({ oauth: { ...session,
|
|
506
|
+
sessionId: randomBytes(16).toString("hex"), binding: sessionBinding(loginConfiguration),
|
|
507
|
+
apiBaseUrl, configuredClientId: loginConfiguration.clientId ?? null,
|
|
508
|
+
} }, parsed.flags, loginConfiguration);
|
|
509
|
+
process.stderr.write(paintErr("green", "Logged in.") + "\n");
|
|
510
|
+
out({ ok: true, method: "device", credentials: credsPath(), ...loginIdentityReport() });
|
|
511
|
+
await flushExit(0);
|
|
498
512
|
}
|
|
499
|
-
async function storePastedToken(
|
|
513
|
+
async function storePastedToken(token, flags) {
|
|
500
514
|
const first = AUTH_SCALARS[0];
|
|
501
515
|
if (!first)
|
|
502
516
|
fail(2, "This API declares no credential the CLI can store. Use --username/--password if it uses basic auth.");
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
out({ ok: true, method: "paste", stored_as: first.flag, credentials: credsPath() });
|
|
517
|
+
await saveLoginCredentials({ scalars: { [first.option]: token } }, flags);
|
|
518
|
+
out({ ok: true, method: "paste", stored_as: first.flag, credentials: credsPath(), ...loginIdentityReport() });
|
|
506
519
|
await flushExit(0);
|
|
507
520
|
}
|
|
508
521
|
/**
|
|
@@ -512,97 +525,139 @@ async function storePastedToken(stored, token) {
|
|
|
512
525
|
* until the API hands back a key minted for this CLI, store it. The key
|
|
513
526
|
* never crosses the chat: the agent relays a URL, the person clicks once.
|
|
514
527
|
*/
|
|
515
|
-
/**
|
|
516
|
-
*
|
|
528
|
+
/** A same-origin API override uses that deployment's approval route. A
|
|
529
|
+
* separately hosted identity service retains its explicitly configured URL. */
|
|
517
530
|
function cliAuthUrl(flags) {
|
|
531
|
+
const auth = loginEndpoint(CLI_AUTH_URL);
|
|
518
532
|
const base = resolveBaseUrl(flags);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (origin !== defaultOrigin && CLI_AUTH_URL.startsWith(defaultOrigin))
|
|
524
|
-
return origin + CLI_AUTH_URL.slice(defaultOrigin.length);
|
|
525
|
-
}
|
|
533
|
+
if (DEFAULT_BASE_URL && base && auth.origin === new URL(DEFAULT_BASE_URL).origin) {
|
|
534
|
+
const selected = loginEndpoint(base);
|
|
535
|
+
auth.protocol = selected.protocol;
|
|
536
|
+
auth.host = selected.host;
|
|
526
537
|
}
|
|
527
|
-
|
|
528
|
-
return CLI_AUTH_URL;
|
|
538
|
+
return auth.href;
|
|
529
539
|
}
|
|
530
|
-
/** Browser approval, start to key: opens (or prints) the approval URL and
|
|
531
|
-
* polls until a person decides. Returns the minted credential; every
|
|
532
|
-
* failure exits with the envelope. Shared by `login` and `init`. */
|
|
533
540
|
async function browserApprove(headless, flags) {
|
|
541
|
+
return withLoginCancellation((signal) => customBrowserApproval({
|
|
542
|
+
authUrl: cliAuthUrl(flags), name: BIN + " CLI on " + hostname(), source: headless && !process.stdin.isTTY ? "agent" : "cli",
|
|
543
|
+
}, { signal, authorize({ verificationUrl, expiresIn }) {
|
|
544
|
+
process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", verificationUrl) + "\n");
|
|
545
|
+
if (headless)
|
|
546
|
+
process.stderr.write(JSON.stringify({ event: "browser_approval", verification_url: verificationUrl, expires_in: expiresIn, note: "Give this URL to the user; polling until they approve or it expires." }) + "\n");
|
|
547
|
+
else
|
|
548
|
+
openInBrowser(verificationUrl);
|
|
549
|
+
} }));
|
|
550
|
+
}
|
|
551
|
+
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
552
|
+
async function storeMinted(minted, flags) {
|
|
534
553
|
const first = AUTH_SCALARS[0];
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const source = headless && !process.stdin.isTTY ? "agent" : "cli";
|
|
540
|
-
let start;
|
|
541
|
-
try {
|
|
542
|
-
const response = await fetch(authUrl + "/start", {
|
|
543
|
-
method: "POST",
|
|
544
|
-
headers: { "Content-Type": "application/json" },
|
|
545
|
-
body: JSON.stringify({ code_challenge: challenge, name, source }),
|
|
546
|
-
signal: AbortSignal.timeout(15_000),
|
|
547
|
-
});
|
|
548
|
-
start = await response.json();
|
|
549
|
-
if (!response.ok || !start.session || !start.verification_url) {
|
|
550
|
-
failWith({ code: "COMMAND_FAILED", message: "Could not start the browser login: " + (start.error ?? "HTTP " + response.status), nextSteps: ["Pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
catch (e) {
|
|
554
|
-
failWith({ code: "NETWORK_ERROR", message: "Could not reach " + authUrl + "/start: " + e.message, nextSteps: ["Check the network, or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
|
|
555
|
-
}
|
|
556
|
-
const url = start.verification_url;
|
|
557
|
-
const expiresIn = start.expires_in ?? 600;
|
|
558
|
-
const intervalMs = Math.max(1, start.interval ?? 3) * 1000;
|
|
559
|
-
process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", url) + "\n");
|
|
560
|
-
if (headless) {
|
|
561
|
-
process.stderr.write(JSON.stringify({ event: "browser_approval", verification_url: url, expires_in: expiresIn, note: "Give this URL to the user; polling until they approve or it expires." }) + "\n");
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
openInBrowser(url);
|
|
565
|
-
}
|
|
566
|
-
const deadline = Date.now() + expiresIn * 1000;
|
|
567
|
-
while (Date.now() < deadline) {
|
|
568
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
569
|
-
let poll;
|
|
554
|
+
await saveLoginCredentials({
|
|
555
|
+
scalars: { [first.option]: minted.api_key },
|
|
556
|
+
minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
|
|
557
|
+
}, flags, undefined, async () => {
|
|
570
558
|
try {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
});
|
|
577
|
-
|
|
559
|
+
const saved = credentialStore().read();
|
|
560
|
+
if ([...Object.values(saved?.scalars ?? {}), ...Object.values(saved?.named ?? {}), saved?.oauth?.accessToken, saved?.oauth?.refreshToken].includes(minted.api_key)) {
|
|
561
|
+
process.stderr.write("The rejected approval returned an existing saved credential; it was not revoked.\n");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const response = await oauthStatusRequest(loginEndpoint(minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + minted.api_key } }, 15_000);
|
|
565
|
+
if (response.status < 200 || response.status >= 300)
|
|
566
|
+
throw new Error("Revocation failed");
|
|
578
567
|
}
|
|
579
568
|
catch {
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
if (poll.status === "pending")
|
|
583
|
-
continue;
|
|
584
|
-
if (poll.status === "complete" && poll.api_key) {
|
|
585
|
-
return { api_key: poll.api_key, key_name: poll.key_name ?? name, ...(poll.org_id ? { org_id: poll.org_id } : {}) };
|
|
569
|
+
process.stderr.write("The rejected approval's new credential could not be revoked. Revoke it in your API account.\n");
|
|
586
570
|
}
|
|
587
|
-
|
|
588
|
-
failWith({ status: "action_required", code: "AUTH_INVALID", message: "The request was denied in the browser.", nextSteps: ["Run '" + BIN + " login' again if that was a mistake, or pass a credential directly with --" + first.flag + "."] });
|
|
589
|
-
if (poll.status === "expired")
|
|
590
|
-
break;
|
|
591
|
-
failWith({ code: "COMMAND_FAILED", message: "Browser login stopped: " + (poll.status ?? "unknown status"), nextSteps: ["Run '" + BIN + " login' again."] });
|
|
592
|
-
}
|
|
593
|
-
failWith({ status: "action_required", code: "TTY_REQUIRED", message: "The browser approval expired after " + expiresIn + "s without a decision.", nextSteps: ["Run '" + BIN + " login' again and approve the link within ten minutes.", "Or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
|
|
594
|
-
}
|
|
595
|
-
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
596
|
-
function storeMinted(stored, minted) {
|
|
597
|
-
const first = AUTH_SCALARS[0];
|
|
598
|
-
stored.scalars = { ...stored.scalars, [first.option]: minted.api_key };
|
|
599
|
-
stored.minted = { via: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}) };
|
|
600
|
-
writeCreds(stored);
|
|
571
|
+
});
|
|
601
572
|
}
|
|
602
|
-
async function browserLogin(
|
|
573
|
+
async function browserLogin(headless, flags) {
|
|
574
|
+
await credentialStore().prepare();
|
|
603
575
|
const minted = await browserApprove(headless, flags);
|
|
604
|
-
storeMinted(
|
|
605
|
-
out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath() });
|
|
576
|
+
await storeMinted(minted, flags);
|
|
577
|
+
out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
578
|
+
await flushExit(0);
|
|
579
|
+
}
|
|
580
|
+
async function acquireOAuthBrowserSession(parsed, clientId) {
|
|
581
|
+
if (!OAUTH_ISSUER)
|
|
582
|
+
fail(2, "Browser OAuth requires the exact auth.oauth_issuer configured by the API owner.");
|
|
583
|
+
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
584
|
+
if (!apiBaseUrl)
|
|
585
|
+
fail(2, "Set the API base URL before logging in.");
|
|
586
|
+
const loginConfiguration = sessionConfiguration(apiBaseUrl);
|
|
587
|
+
await credentialStore().prepare();
|
|
588
|
+
const session = await startOAuthBrowserSession(parsed, clientId);
|
|
589
|
+
await saveLoginCredentials({ oauth: { ...session, apiBaseUrl,
|
|
590
|
+
sessionId: randomBytes(16).toString("hex"), binding: sessionBinding(loginConfiguration),
|
|
591
|
+
configuredClientId: loginConfiguration.clientId ?? null,
|
|
592
|
+
} }, parsed.flags, loginConfiguration);
|
|
593
|
+
}
|
|
594
|
+
/** Normal login and Console verification use the same native exchange. */
|
|
595
|
+
async function startOAuthBrowserSession(parsed, clientId, timeoutMs) {
|
|
596
|
+
const controller = new AbortController();
|
|
597
|
+
const cancel = () => controller.abort();
|
|
598
|
+
process.once("SIGINT", cancel);
|
|
599
|
+
process.once("SIGTERM", cancel);
|
|
600
|
+
try {
|
|
601
|
+
return await oauthBrowserLogin({
|
|
602
|
+
issuer: OAUTH_ISSUER, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
|
|
603
|
+
authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
|
|
604
|
+
redirectUri: OAUTH_REDIRECT_URI, scopes: OAUTH_SCOPES,
|
|
605
|
+
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
606
|
+
organization: requestedLoginOrganization(parsed.flags),
|
|
607
|
+
}, { signal: controller.signal, timeoutMs, authorize(url) {
|
|
608
|
+
process.stderr.write("Sign in to your existing account: " + url + "\n");
|
|
609
|
+
if (isAgentMode(parsed) || parsed.flags.get("no-browser") === true)
|
|
610
|
+
process.stderr.write(JSON.stringify({ event: "oauth_browser", authorization_url: url, note: "Open this URL in a browser on the same computer as the CLI." }) + "\n");
|
|
611
|
+
else
|
|
612
|
+
openInBrowser(url);
|
|
613
|
+
} });
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
process.off("SIGINT", cancel);
|
|
617
|
+
process.off("SIGTERM", cancel);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function cmdConsoleLoginCheck(parsed) {
|
|
621
|
+
const file = parsed.flags.get("console-check");
|
|
622
|
+
if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed))
|
|
623
|
+
fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
|
|
624
|
+
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
625
|
+
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
626
|
+
const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
|
|
627
|
+
if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length)
|
|
628
|
+
fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
|
|
629
|
+
const envOptions = {}, flagOptions = {};
|
|
630
|
+
for (const scalar of AUTH_SCALARS) {
|
|
631
|
+
if (process.env[scalar.env] !== undefined)
|
|
632
|
+
envOptions[scalar.option] = process.env[scalar.env];
|
|
633
|
+
if (typeof parsed.flags.get(scalar.flag) === "string")
|
|
634
|
+
flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
|
|
635
|
+
}
|
|
636
|
+
if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass])
|
|
637
|
+
envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
638
|
+
if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string")
|
|
639
|
+
flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
|
|
640
|
+
const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl).href)?.[0]) ?? null;
|
|
641
|
+
const result = await checkConsoleBrowserLogin({
|
|
642
|
+
file: file, configuration: {
|
|
643
|
+
baseUrl: baseUrl, environment, operation: op.resource + "." + op.method, requirements: op.security,
|
|
644
|
+
request: { method: op.graphql ? "POST" : op.httpMethod, path: op.graphql ? "" : op.path, graphqlField: op.graphql?.field ?? null, graphqlQuery: op.graphql ? op.graphql.docPrefix + op.graphql.defaultSelection + " }" : null },
|
|
645
|
+
issuer: OAUTH_ISSUER, clientId: clientId, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
|
|
646
|
+
authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
|
|
647
|
+
redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
|
|
648
|
+
audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
|
|
649
|
+
}, schemes: NAMED_SCHEMES,
|
|
650
|
+
credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
|
|
651
|
+
login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId, timeoutMs),
|
|
652
|
+
async verify(credentials, expectations) {
|
|
653
|
+
const kind = (value) => value === "user" ? "subject" : value;
|
|
654
|
+
const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer]));
|
|
655
|
+
const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)]));
|
|
656
|
+
await verifyClientIdentity((options) => new TypeshipClient(options), { baseUrl: baseUrl, credentials }, op, policy, expected);
|
|
657
|
+
},
|
|
658
|
+
progress: (message) => process.stderr.write(message + "\n"),
|
|
659
|
+
});
|
|
660
|
+
out(result);
|
|
606
661
|
await flushExit(0);
|
|
607
662
|
}
|
|
608
663
|
async function cmdLogin(parsed) {
|
|
@@ -613,16 +668,34 @@ async function cmdLogin(parsed) {
|
|
|
613
668
|
...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
|
|
614
669
|
...(CLI_AUTH_URL ? [" " + BIN + " login approve in the browser: a key is minted for you (add --no-browser to print the link instead of opening it)"] : []),
|
|
615
670
|
" " + BIN + " login --with-token read the credential from stdin (CI)",
|
|
616
|
-
|
|
671
|
+
" " + BIN + " login --console-check <file> test browser login for your Typeship Console without replacing saved logins",
|
|
672
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + OAUTH_LOGIN_METHOD + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "")] : []),
|
|
673
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --no-browser print the approval URL instead of opening a browser", " " + BIN + " login --device use device authorization when your provider supports it"] : []),
|
|
617
674
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
618
|
-
...(CLI_AUTH_URL ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
675
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
619
676
|
"",
|
|
620
|
-
"Precedence
|
|
677
|
+
"Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
|
|
678
|
+
" " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
|
|
679
|
+
"Named schemes: " + Object.entries(NAMED_SCHEMES).map(([name, scheme]) => name + " (" + scheme.kind + ")").join(", "),
|
|
680
|
+
"JSON values are tokens/API keys, or {username, password} for Basic auth. Runtime env: TYPESHIP_CREDENTIALS.",
|
|
681
|
+
"--profile <name> selects an isolated login. Use auth profiles to list profiles and auth use <name> to select a default.",
|
|
682
|
+
...(Object.keys(IDENTITY_POLICY).length ? ["Login verifies your API identity before saving. Add --subject <id>, --account <id> or --organization <id> to require a particular mapped identity."] : []),
|
|
683
|
+
...(OAUTH_ORGANIZATION_PARAMETER ? ["--login-organization <id> requests that provider organization during browser login and requires the API to confirm the same ID."] : []),
|
|
684
|
+
"Saved credentials use OS protection by default. TYPESHIP_CREDENTIAL_STORE=file explicitly opts into plaintext storage.",
|
|
621
685
|
];
|
|
622
686
|
process.stdout.write(lines.join("\n") + "\n");
|
|
623
687
|
await flushExit(0);
|
|
624
688
|
}
|
|
625
|
-
|
|
689
|
+
if (parsed.flags.has("login-organization")) {
|
|
690
|
+
expectedLoginIdentity(parsed.flags);
|
|
691
|
+
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("console-check") || parsed.flags.has("with-token") || explicitNonInteractive(parsed))
|
|
692
|
+
fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
|
|
693
|
+
}
|
|
694
|
+
if (parsed.flags.has("console-check")) {
|
|
695
|
+
await cmdConsoleLoginCheck(parsed);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const named = flagCredentials(parsed.flags);
|
|
626
699
|
const scalarValues = {};
|
|
627
700
|
for (const a of AUTH_SCALARS) {
|
|
628
701
|
const v = parsed.flags.get(a.flag);
|
|
@@ -632,31 +705,38 @@ async function cmdLogin(parsed) {
|
|
|
632
705
|
const username = parsed.flags.get("username");
|
|
633
706
|
const password = parsed.flags.get("password");
|
|
634
707
|
const gotBasic = BASIC !== null && typeof username === "string" && typeof password === "string";
|
|
635
|
-
if (Object.keys(scalarValues).length > 0 || gotBasic) {
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
708
|
+
if (Object.keys(scalarValues).length > 0 || gotBasic || Object.keys(named).length > 0) {
|
|
709
|
+
if (parsed.flags.has("login-organization"))
|
|
710
|
+
fail(2, "--login-organization cannot be combined with pasted credentials.");
|
|
711
|
+
await saveLoginCredentials({
|
|
712
|
+
...(Object.keys(named).length ? { named } : {}),
|
|
713
|
+
...(Object.keys(scalarValues).length > 0 ? { scalars: scalarValues } : {}),
|
|
714
|
+
...(gotBasic ? { basic: { username: username, password: password } } : {}),
|
|
715
|
+
}, parsed.flags);
|
|
716
|
+
out({ ok: true, method: "flags", credentials: credsPath(), ...loginIdentityReport() });
|
|
642
717
|
await flushExit(0);
|
|
643
718
|
}
|
|
644
719
|
if (parsed.flags.get("with-token") === true) {
|
|
645
720
|
const token = (await readStdin()).trim();
|
|
646
721
|
if (!token)
|
|
647
722
|
fail(2, "--with-token expects the credential on stdin.");
|
|
648
|
-
await storePastedToken(
|
|
723
|
+
await storePastedToken(token, parsed.flags);
|
|
649
724
|
}
|
|
650
725
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined)
|
|
651
726
|
?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
|
|
652
|
-
if (
|
|
653
|
-
|
|
727
|
+
if (HAS_OAUTH_LOGIN && clientId !== undefined && !explicitNonInteractive(parsed)) {
|
|
728
|
+
if (OAUTH_LOGIN_METHOD === "browser" && parsed.flags.get("device") !== true) {
|
|
729
|
+
await acquireOAuthBrowserSession(parsed, clientId);
|
|
730
|
+
out({ ok: true, method: "oauth_browser", credentials: credsPath(), ...loginIdentityReport() });
|
|
731
|
+
await flushExit(0);
|
|
732
|
+
}
|
|
733
|
+
await deviceLogin(clientId, parsed);
|
|
654
734
|
}
|
|
655
735
|
// Browser approval: the API mints a key for this CLI once a person
|
|
656
736
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
657
737
|
// polls); only the explicit non-interactive switch turns it off.
|
|
658
738
|
if (CLI_AUTH_URL && AUTH_SCALARS[0] && !explicitNonInteractive(parsed)) {
|
|
659
|
-
await browserLogin(
|
|
739
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
660
740
|
}
|
|
661
741
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
662
742
|
failWith({
|
|
@@ -666,7 +746,7 @@ async function cmdLogin(parsed) {
|
|
|
666
746
|
nextSteps: [
|
|
667
747
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
668
748
|
"Pipe it: echo \"$TOKEN\" | " + BIN + " login --with-token",
|
|
669
|
-
...(
|
|
749
|
+
...(HAS_OAUTH_LOGIN ? ["OAuth login (or --device when supported by your provider): '" + BIN + " login --client-id <id>' (opens or prints the provider sign-in URL)."] : []),
|
|
670
750
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
671
751
|
],
|
|
672
752
|
});
|
|
@@ -677,34 +757,49 @@ async function cmdLogin(parsed) {
|
|
|
677
757
|
const token = (await promptHidden("Paste " + first.flag.replace(/-/g, " ") + " (input hidden): ")).trim();
|
|
678
758
|
if (!token)
|
|
679
759
|
fail(2, "Nothing entered.");
|
|
680
|
-
await storePastedToken(
|
|
760
|
+
await storePastedToken(token, parsed.flags);
|
|
681
761
|
}
|
|
682
|
-
async function cmdLogout() {
|
|
683
|
-
|
|
684
|
-
|
|
762
|
+
async function cmdLogout(parsed) {
|
|
763
|
+
if (parsed.flags.get("local") === true) {
|
|
764
|
+
const removed = await credentialStore().clear();
|
|
765
|
+
out({ ok: true, removed: removed ? credsPath() : null, revoked: false, revocation_skipped: true });
|
|
766
|
+
await flushExit(0);
|
|
767
|
+
}
|
|
768
|
+
const stored = await credentialStore().take();
|
|
769
|
+
const existed = stored !== null;
|
|
685
770
|
// A key this CLI minted for itself (browser approval) is revoked on the
|
|
686
771
|
// way out, so logging out ends the credential and not just the file. A
|
|
687
772
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
688
773
|
let revoked = null;
|
|
689
774
|
const first = AUTH_SCALARS[0];
|
|
690
775
|
const ownKey = first && stored?.minted?.via === "browser" ? stored.scalars?.[first.option] : undefined;
|
|
691
|
-
if (ownKey
|
|
776
|
+
if (ownKey) {
|
|
692
777
|
try {
|
|
693
|
-
const response = await
|
|
694
|
-
revoked = response.
|
|
778
|
+
const response = await oauthStatusRequest(loginEndpoint(stored.minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
|
|
779
|
+
revoked = response.status >= 200 && response.status < 300;
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
revoked = false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (stored?.oauth?.revocationUrl) {
|
|
786
|
+
try {
|
|
787
|
+
const oauth = stored.oauth;
|
|
788
|
+
const token = oauth.refreshToken ?? oauth.accessToken;
|
|
789
|
+
const result = await oauthStatusRequest(loginEndpoint(oauth.revocationUrl), { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ token, token_type_hint: oauth.refreshToken ? "refresh_token" : "access_token", ...(oauth.clientId ? { client_id: oauth.clientId } : {}) }) });
|
|
790
|
+
revoked = result.status >= 200 && result.status < 300;
|
|
695
791
|
}
|
|
696
792
|
catch {
|
|
697
793
|
revoked = false;
|
|
698
794
|
}
|
|
699
795
|
}
|
|
700
|
-
rmSync(credsPath(), { force: true });
|
|
701
796
|
out({ ok: true, removed: existed ? credsPath() : null, ...(revoked === null ? {} : { revoked, key_name: stored?.minted?.key_name }) });
|
|
702
797
|
await flushExit(0);
|
|
703
798
|
}
|
|
704
799
|
async function cmdWhoami(parsed) {
|
|
705
800
|
const op = WHOAMI ? OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method) : undefined;
|
|
706
801
|
if (op) {
|
|
707
|
-
const client = await makeClient(parsed.flags);
|
|
802
|
+
const client = await makeClient(parsed.flags, op);
|
|
708
803
|
const target = client[op.resource];
|
|
709
804
|
const result = await target[op.method]();
|
|
710
805
|
if (result.ok) {
|
|
@@ -713,36 +808,14 @@ async function cmdWhoami(parsed) {
|
|
|
713
808
|
}
|
|
714
809
|
failApi(result.error, LAST_CLIENT_HAD_CREDENTIAL);
|
|
715
810
|
}
|
|
716
|
-
const
|
|
717
|
-
let source = "none";
|
|
718
|
-
if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string")) {
|
|
719
|
-
source = "flags";
|
|
720
|
-
}
|
|
721
|
-
else {
|
|
722
|
-
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
723
|
-
if (envScalar)
|
|
724
|
-
source = "env:" + envScalar.env;
|
|
725
|
-
else if (stored && (stored.scalars || stored.basic || stored.oauth))
|
|
726
|
-
source = "login";
|
|
727
|
-
}
|
|
811
|
+
const source = credentialSource(parsed.flags) ?? "none";
|
|
728
812
|
out({ authenticated: source !== "none", source, credentials: existsSync(credsPath()) ? credsPath() : null });
|
|
729
813
|
await flushExit(source === "none" ? 1 : 0);
|
|
730
814
|
}
|
|
731
815
|
function configFilePath() {
|
|
732
816
|
return join(configDir(), "config.json");
|
|
733
817
|
}
|
|
734
|
-
function readConfig() {
|
|
735
|
-
try {
|
|
736
|
-
return JSON.parse(readFileSync(configFilePath(), "utf8"));
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
return {};
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
function writeConfig(config) {
|
|
743
|
-
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
744
|
-
writeFileSync(configFilePath(), JSON.stringify(config, null, 2) + "\n");
|
|
745
|
-
}
|
|
818
|
+
function readConfig() { return readProfileConfig(configDir()); }
|
|
746
819
|
const CONFIG_KEYS = ["base-url", "environment", "docs-url"];
|
|
747
820
|
function configGet(config, key) {
|
|
748
821
|
return key === "base-url" ? config.baseUrl : key === "environment" ? config.environment : config.docsUrl;
|
|
@@ -773,6 +846,7 @@ async function cmdConfig(parsed) {
|
|
|
773
846
|
if (sub === undefined || sub === "list") {
|
|
774
847
|
const config = readConfig();
|
|
775
848
|
out({
|
|
849
|
+
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
776
850
|
base_url: config.baseUrl ?? null,
|
|
777
851
|
environment: config.environment ?? null,
|
|
778
852
|
docs_url: config.docsUrl ?? DOCS_URL_DEFAULT,
|
|
@@ -795,13 +869,16 @@ async function cmdConfig(parsed) {
|
|
|
795
869
|
await flushExit(0);
|
|
796
870
|
}
|
|
797
871
|
if (sub === "unset") {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
872
|
+
await updateProfileConfig(configDir(), (current) => {
|
|
873
|
+
const next = { ...current };
|
|
874
|
+
if (key === "base-url")
|
|
875
|
+
delete next.baseUrl;
|
|
876
|
+
else if (key === "environment")
|
|
877
|
+
delete next.environment;
|
|
878
|
+
else
|
|
879
|
+
delete next.docsUrl;
|
|
880
|
+
return next;
|
|
881
|
+
});
|
|
805
882
|
out({ ok: true });
|
|
806
883
|
await flushExit(0);
|
|
807
884
|
}
|
|
@@ -814,10 +891,6 @@ async function cmdConfig(parsed) {
|
|
|
814
891
|
catch {
|
|
815
892
|
fail(2, key + " must be a valid URL");
|
|
816
893
|
}
|
|
817
|
-
if (key === "base-url")
|
|
818
|
-
config.baseUrl = value;
|
|
819
|
-
else
|
|
820
|
-
config.docsUrl = value;
|
|
821
894
|
}
|
|
822
895
|
else {
|
|
823
896
|
if (!(value in ENVIRONMENTS)) {
|
|
@@ -825,9 +898,8 @@ async function cmdConfig(parsed) {
|
|
|
825
898
|
? "environment must be one of: " + Object.keys(ENVIRONMENTS).join(", ")
|
|
826
899
|
: "The spec declares no named environments; use 'config set base-url' instead.");
|
|
827
900
|
}
|
|
828
|
-
config.environment = value;
|
|
829
901
|
}
|
|
830
|
-
|
|
902
|
+
await updateProfileConfig(configDir(), (current) => key === "base-url" ? { ...current, baseUrl: value } : key === "docs-url" ? { ...current, docsUrl: value } : { ...current, environment: value, baseUrl: undefined });
|
|
831
903
|
out({ ok: true, [key.replace(/-/g, "_")]: value });
|
|
832
904
|
await flushExit(0);
|
|
833
905
|
}
|
|
@@ -848,15 +920,6 @@ function mcpServerPath() {
|
|
|
848
920
|
}
|
|
849
921
|
return { path, warning };
|
|
850
922
|
}
|
|
851
|
-
function claudeDesktopConfigPath() {
|
|
852
|
-
if (process.platform === "darwin") {
|
|
853
|
-
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
854
|
-
}
|
|
855
|
-
if (process.platform === "win32") {
|
|
856
|
-
return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
857
|
-
}
|
|
858
|
-
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "Claude", "claude_desktop_config.json");
|
|
859
|
-
}
|
|
860
923
|
/** The mcp entry for a client: the hosted endpoint when the API has one
|
|
861
924
|
* (auth env var as a reference, never a literal), else the local stdio
|
|
862
925
|
* server. --url overrides. */
|
|
@@ -878,7 +941,12 @@ function mcpEntryFor(url, readOnly = false) {
|
|
|
878
941
|
const server = mcpServerPath();
|
|
879
942
|
if (server.warning)
|
|
880
943
|
warnings.push(server.warning);
|
|
881
|
-
|
|
944
|
+
const environment = {
|
|
945
|
+
...(process.env.XDG_CONFIG_HOME !== undefined ? { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME } : {}),
|
|
946
|
+
...(process.env["TYPESHIP_CREDENTIAL_STORE"] !== undefined ? { "TYPESHIP_CREDENTIAL_STORE": process.env["TYPESHIP_CREDENTIAL_STORE"] } : {}),
|
|
947
|
+
};
|
|
948
|
+
const pinned = PROFILE.source === "flag" || PROFILE.source === "environment";
|
|
949
|
+
return { entry: { command: "node", args: [server.path, ...(pinned ? ["--profile", PROFILE.name] : []), ...(readOnly ? ["--read-only"] : [])], ...(Object.keys(environment).length ? { env: environment } : {}) }, warnings };
|
|
882
950
|
}
|
|
883
951
|
const MCP_CLIENT_FLAGS = {
|
|
884
952
|
claude: "claude-code", cursor: "cursor", "claude-desktop": "claude-desktop", codex: "codex", vscode: "vscode",
|
|
@@ -900,11 +968,20 @@ async function cmdMcp(parsed) {
|
|
|
900
968
|
" " + BIN + " mcp install --claude --read-only register a read-only server (writes are not callable)",
|
|
901
969
|
"",
|
|
902
970
|
(MCP_URL ? "Default entry: the hosted endpoint " + MCP_URL + " with the auth env var as a reference (never a literal key)." : "Default entry: this package's local stdio server, which reads credentials saved by '" + BIN + " login' or the CLI's auth env vars."),
|
|
903
|
-
"
|
|
971
|
+
"--all skips Cursor until it speaks MCP 2026-07-28.",
|
|
904
972
|
];
|
|
905
973
|
process.stdout.write(lines.join("\n") + "\n");
|
|
906
974
|
await flushExit(0);
|
|
907
975
|
}
|
|
976
|
+
const sub = parsed.positionals[1];
|
|
977
|
+
if (sub !== undefined && sub !== "install") {
|
|
978
|
+
fail(2, "Unknown mcp command: " + sub + ". Try '" + BIN + " mcp install --all'.");
|
|
979
|
+
}
|
|
980
|
+
const requestedInstall = parsed.flags.get("all") === true
|
|
981
|
+
|| Object.keys(MCP_CLIENT_FLAGS).some((flag) => parsed.flags.get(flag) === true);
|
|
982
|
+
if (requestedInstall && sub !== "install") {
|
|
983
|
+
fail(2, "Installing an MCP entry requires '" + BIN + " mcp install'.");
|
|
984
|
+
}
|
|
908
985
|
const url = typeof parsed.flags.get("url") === "string" ? parsed.flags.get("url") : undefined;
|
|
909
986
|
if (url) {
|
|
910
987
|
try {
|
|
@@ -968,8 +1045,11 @@ function agentContext() {
|
|
|
968
1045
|
apiTitle: API_TITLE,
|
|
969
1046
|
version: VERSION,
|
|
970
1047
|
envPrefix: ENV_PREFIX,
|
|
971
|
-
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : [])],
|
|
1048
|
+
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
|
|
972
1049
|
docsUrl: docsSiteUrl(),
|
|
1050
|
+
docsIndexUrl: docsIndexUrl(),
|
|
1051
|
+
generatedOperationCount: OPS.length,
|
|
1052
|
+
omittedOperations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
|
|
973
1053
|
mcpUrl: MCP_URL,
|
|
974
1054
|
skillsRepo: SKILLS_REPO,
|
|
975
1055
|
hasMcp: HAS_MCP,
|
|
@@ -1028,13 +1108,21 @@ function helpJson() {
|
|
|
1028
1108
|
};
|
|
1029
1109
|
}),
|
|
1030
1110
|
})),
|
|
1111
|
+
...(EXCLUDED_OPS > 0 ? {
|
|
1112
|
+
coverage: {
|
|
1113
|
+
generated_operations: OPS.length,
|
|
1114
|
+
total_operations: OPS.length + EXCLUDED_OPS,
|
|
1115
|
+
omitted_operations: OMITTED_OPS.map((op) => ({ command: op.command.join(" "), tool: op.tool, method: op.httpMethod, path: op.path })),
|
|
1116
|
+
reason: "plan_limit",
|
|
1117
|
+
},
|
|
1118
|
+
} : {}),
|
|
1031
1119
|
discovery: {
|
|
1032
1120
|
search: BIN + " docs search <term> --json",
|
|
1033
1121
|
operation: BIN + " docs <resource> <command> --json",
|
|
1034
1122
|
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1035
1123
|
},
|
|
1036
1124
|
builtins: BUILTIN_COMMANDS,
|
|
1037
|
-
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--base-url <url>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
|
|
1125
|
+
global_flags: ["--help", "--version", "--debug", "--non-interactive", "--mode agent|human", "--yes", "--force", "--color on|off|auto", "--credentials @<JSON-file>|-", "--base-url <url>", "--profile <name>", "--data '<json>' | @<file> | -", "--fields <a,b.c>", "--all", "--validate", "--out <dir>", ...AUTH_SCALARS.map((a) => "--" + a.flag + " <value>")],
|
|
1038
1126
|
auth_env_vars: agentContext().authEnvVars,
|
|
1039
1127
|
};
|
|
1040
1128
|
}
|
|
@@ -1049,32 +1137,48 @@ async function cmdAgentGuide(parsed) {
|
|
|
1049
1137
|
/** Which credential the CLI would send, without sending it. --live calls the identity endpoint too. */
|
|
1050
1138
|
async function cmdAuth(parsed) {
|
|
1051
1139
|
const sub = parsed.positionals[1];
|
|
1140
|
+
if (!parsed.help && sub === "profiles") {
|
|
1141
|
+
out({ profile: PROFILE.name, source: PROFILE.source, profiles: listProfiles(configRoot()).map((entry) => ({ name: entry.name, selected: entry.name === PROFILE.name, has_saved_credentials: entry.hasSavedCredentials, base_url: entry.config.baseUrl ?? (entry.config.environment ? ENVIRONMENTS[entry.config.environment] : undefined) ?? DEFAULT_BASE_URL, environment: entry.config.environment ?? null })) });
|
|
1142
|
+
await flushExit(0);
|
|
1143
|
+
}
|
|
1144
|
+
if (!parsed.help && (sub === "use" || sub === "remove")) {
|
|
1145
|
+
const name = parsed.positionals[2];
|
|
1146
|
+
if (!name)
|
|
1147
|
+
fail(2, "auth " + sub + " requires a profile name.");
|
|
1148
|
+
if (sub === "use")
|
|
1149
|
+
selectProfile(configRoot(), name);
|
|
1150
|
+
else
|
|
1151
|
+
await removeProfile(configRoot(), name);
|
|
1152
|
+
const effective = resolveProfile(configRoot(), { flag: typeof parsed.flags.get("profile") === "string" ? parsed.flags.get("profile") : undefined, environment: process.env["TYPESHIP_PROFILE"], allowMissing: true });
|
|
1153
|
+
out({ ok: true, ...(sub === "use" ? { selected: name } : { removed: name }), profile: effective.name, source: effective.source });
|
|
1154
|
+
await flushExit(0);
|
|
1155
|
+
}
|
|
1052
1156
|
if (parsed.help || sub !== "check") {
|
|
1053
1157
|
process.stdout.write([
|
|
1054
1158
|
BIN + " auth check [--live] — report the credential the CLI would use, as JSON: {status: ok|action_required, authenticated, source, ...}",
|
|
1159
|
+
" " + BIN + " auth profiles list profiles without unlocking credentials",
|
|
1160
|
+
" " + BIN + " auth use <name> select the default profile",
|
|
1161
|
+
" " + BIN + " auth remove <name> remove a profile after logout",
|
|
1162
|
+
" --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
|
|
1055
1163
|
" --live also call the API's identity endpoint" + (WHOAMI ? "" : " (none in this API; --live is a no-op)"),
|
|
1056
1164
|
"",
|
|
1057
1165
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1058
1166
|
].join("\n") + "\n");
|
|
1059
1167
|
await flushExit(parsed.help ? 0 : 2);
|
|
1060
1168
|
}
|
|
1061
|
-
const
|
|
1062
|
-
let source = "none";
|
|
1063
|
-
if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string"))
|
|
1064
|
-
source = "flags";
|
|
1065
|
-
else {
|
|
1066
|
-
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
1067
|
-
if (envScalar)
|
|
1068
|
-
source = "env:" + envScalar.env;
|
|
1069
|
-
else if (stored && (stored.scalars || stored.basic || stored.oauth))
|
|
1070
|
-
source = "login";
|
|
1071
|
-
}
|
|
1169
|
+
const source = credentialSource(parsed.flags) ?? "none";
|
|
1072
1170
|
const authenticated = source !== "none";
|
|
1171
|
+
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1172
|
+
if (savedIdentity)
|
|
1173
|
+
assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1073
1174
|
const report = {
|
|
1074
1175
|
status: authenticated ? "ok" : "action_required",
|
|
1075
1176
|
authenticated,
|
|
1076
1177
|
source,
|
|
1077
1178
|
credentials_path: existsSync(credsPath()) ? credsPath() : null,
|
|
1179
|
+
credential_storage: credentialStore().backend,
|
|
1180
|
+
verified_identity: savedIdentity?.identity?.values ?? null, identity_checked_at: savedIdentity?.identity?.checkedAt ?? null,
|
|
1181
|
+
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
1078
1182
|
auth_env_vars: agentContext().authEnvVars,
|
|
1079
1183
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1080
1184
|
next_steps: authenticated ? [] : [
|
|
@@ -1085,11 +1189,14 @@ async function cmdAuth(parsed) {
|
|
|
1085
1189
|
if (authenticated && parsed.flags.get("live") === true && WHOAMI) {
|
|
1086
1190
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1087
1191
|
if (op) {
|
|
1088
|
-
const client = await makeClient(parsed.flags);
|
|
1192
|
+
const client = await makeClient(parsed.flags, op);
|
|
1089
1193
|
const target = client[op.resource];
|
|
1090
1194
|
const result = await target[op.method]();
|
|
1091
|
-
if (result.ok)
|
|
1195
|
+
if (result.ok) {
|
|
1196
|
+
if (identityConfiguration() && savedIdentity?.identity)
|
|
1197
|
+
assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1092
1198
|
report.identity = result.data;
|
|
1199
|
+
}
|
|
1093
1200
|
else {
|
|
1094
1201
|
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1095
1202
|
report.status = "action_required";
|
|
@@ -1110,10 +1217,16 @@ async function cmdDoctor(parsed) {
|
|
|
1110
1217
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
1111
1218
|
checks.push({ name: "node", ok: nodeMajor >= 18, detail: process.version, ...(nodeMajor >= 18 ? {} : { fix: "Install Node 18 or newer." }) });
|
|
1112
1219
|
checks.push({ name: "cli", ok: true, detail: BIN + " " + VERSION + " (" + PKG_NAME + ")" });
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1220
|
+
let source = null;
|
|
1221
|
+
let storageProblem;
|
|
1222
|
+
try {
|
|
1223
|
+
source = credentialSource(parsed.flags);
|
|
1224
|
+
}
|
|
1225
|
+
catch (error) {
|
|
1226
|
+
storageProblem = error.message;
|
|
1227
|
+
}
|
|
1228
|
+
const hasCred = source !== null;
|
|
1229
|
+
checks.push({ name: "credentials", ok: hasCred, detail: storageProblem ?? (source === "login" ? credentialStore().backend : source ?? "none"), ...(hasCred ? {} : { fix: storageProblem ?? (AUTH_SCALARS[0] ? "Set " + AUTH_SCALARS[0].env + " or run '" + BIN + " login'." : "Run '" + BIN + " login'.") }) });
|
|
1117
1230
|
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
1118
1231
|
if (baseUrl) {
|
|
1119
1232
|
try {
|
|
@@ -1131,7 +1244,7 @@ async function cmdDoctor(parsed) {
|
|
|
1131
1244
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1132
1245
|
if (op) {
|
|
1133
1246
|
try {
|
|
1134
|
-
const client = await makeClient(parsed.flags);
|
|
1247
|
+
const client = await makeClient(parsed.flags, op);
|
|
1135
1248
|
const target = client[op.resource];
|
|
1136
1249
|
const result = await target[op.method]();
|
|
1137
1250
|
checks.push(result.ok ? { name: "identity", ok: true, detail: op.command.join(" ") + " ok" } : { name: "identity", ok: false, detail: classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT }).message, fix: "The credential was rejected; run '" + BIN + " login' with a current one." });
|
|
@@ -1184,27 +1297,39 @@ async function cmdInit(parsed) {
|
|
|
1184
1297
|
const cwd = process.cwd();
|
|
1185
1298
|
const harness = detectHarness();
|
|
1186
1299
|
// 1. credential
|
|
1187
|
-
const stored = readCreds() ?? {};
|
|
1188
1300
|
const first = AUTH_SCALARS[0];
|
|
1301
|
+
const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
|
|
1302
|
+
const stored = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
|
|
1189
1303
|
const given = (typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined)
|
|
1190
1304
|
?? (first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined);
|
|
1191
|
-
if (
|
|
1192
|
-
|
|
1193
|
-
|
|
1305
|
+
if (Object.keys(named).length) {
|
|
1306
|
+
await saveLoginCredentials({ named }, parsed.flags);
|
|
1307
|
+
report.credential = { status: "stored", path: credsPath() };
|
|
1308
|
+
}
|
|
1309
|
+
else if (given && first) {
|
|
1310
|
+
await saveLoginCredentials({ scalars: { [first.option]: given } }, parsed.flags);
|
|
1194
1311
|
report.credential = { status: "stored", path: credsPath() };
|
|
1195
1312
|
}
|
|
1196
1313
|
else if (first && process.env[first.env]) {
|
|
1197
1314
|
report.credential = { status: "env", variable: first.env };
|
|
1198
1315
|
}
|
|
1199
|
-
else if (
|
|
1316
|
+
else if (Object.keys(envNamed).length) {
|
|
1317
|
+
report.credential = { status: "env", variable: "TYPESHIP_CREDENTIALS" };
|
|
1318
|
+
}
|
|
1319
|
+
else if (stored.scalars || stored.basic || stored.oauth || stored.named) {
|
|
1200
1320
|
report.credential = { status: "stored", path: credsPath() };
|
|
1201
1321
|
}
|
|
1322
|
+
else if (first && HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" && (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID) && !explicitNonInteractive(parsed)) {
|
|
1323
|
+
await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID));
|
|
1324
|
+
report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
|
|
1325
|
+
}
|
|
1202
1326
|
else if (first && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1203
1327
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1204
1328
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1205
1329
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1330
|
+
await credentialStore().prepare();
|
|
1206
1331
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1207
|
-
storeMinted(
|
|
1332
|
+
await storeMinted(minted, parsed.flags);
|
|
1208
1333
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1209
1334
|
}
|
|
1210
1335
|
else {
|
|
@@ -1315,7 +1440,7 @@ async function cmdUpgrade(parsed) {
|
|
|
1315
1440
|
fail(1, "This run came through npx, which fetches the latest version each time; there is nothing to upgrade in place.");
|
|
1316
1441
|
if (manager !== "npm") {
|
|
1317
1442
|
const command = manager === "pnpm" ? "pnpm add -g " + PKG_NAME + "@" + latest : manager === "bun" ? "bun add -g " + PKG_NAME + "@" + latest : "yarn global add " + PKG_NAME + "@" + latest;
|
|
1318
|
-
failWith({ status: "action_required", code: "
|
|
1443
|
+
failWith({ status: "action_required", code: "CALL_FAILED", message: PKG_NAME + " was installed with " + manager + ", so npm can't upgrade it in place.", nextSteps: ["Run: " + command] });
|
|
1319
1444
|
}
|
|
1320
1445
|
process.stderr.write("Upgrading " + PKG_NAME + " " + VERSION + " -> " + latest + "\n");
|
|
1321
1446
|
// Windows resolves npm to npm.cmd, which only a shell can start.
|
|
@@ -1329,7 +1454,7 @@ async function cmdUpgrade(parsed) {
|
|
|
1329
1454
|
// ---------------------------------------------------------------------------
|
|
1330
1455
|
// completion — shell completion scripts built from the op table
|
|
1331
1456
|
// ---------------------------------------------------------------------------
|
|
1332
|
-
const TOP_WORDS = ["login", "logout", "whoami", "config", "mcp", "
|
|
1457
|
+
const TOP_WORDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
|
|
1333
1458
|
/** Every flag an API command accepts, with the values a flag completes to (enums, on|off|auto, agent|human). */
|
|
1334
1459
|
function completionFlagsFor(op) {
|
|
1335
1460
|
const flags = [...op.params.filter((p) => p.kind !== "path").map((p) => "--" + p.flag), ...COMPLETION_GLOBAL_FLAGS];
|
|
@@ -1343,13 +1468,12 @@ function completionFlagsFor(op) {
|
|
|
1343
1468
|
}
|
|
1344
1469
|
return { flags, values };
|
|
1345
1470
|
}
|
|
1346
|
-
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--base-url", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
|
|
1471
|
+
const COMPLETION_GLOBAL_FLAGS = ["--help", "--version", "--non-interactive", "--color", "--credentials", "--base-url", "--profile", "--data", "--fields", "--all", "--validate", "--debug", "--mode", "--yes", "--force", "--out", ...AUTH_SCALARS.map((a) => "--" + a.flag)];
|
|
1347
1472
|
const BUILTIN_WORDS = {
|
|
1348
1473
|
config: ["list", "get", "set", "unset", "path"],
|
|
1349
1474
|
completion: ["bash", "zsh", "fish"],
|
|
1350
1475
|
auth: ["check"],
|
|
1351
1476
|
mcp: ["install"],
|
|
1352
|
-
webhooks: ["listen", "fake"],
|
|
1353
1477
|
};
|
|
1354
1478
|
/** bash (and zsh via bashcompinit): resources, commands, flags, and the values a flag takes. */
|
|
1355
1479
|
function completionScript() {
|
|
@@ -1446,16 +1570,18 @@ async function cmdCompletion(parsed) {
|
|
|
1446
1570
|
function docsSiteUrl() {
|
|
1447
1571
|
return readConfig().docsUrl ?? DOCS_URL_DEFAULT;
|
|
1448
1572
|
}
|
|
1573
|
+
function docsIndexUrl() {
|
|
1574
|
+
const configured = readConfig().docsUrl;
|
|
1575
|
+
return configured
|
|
1576
|
+
? resolveDocsContentUrl(configured, null, "llms.txt")
|
|
1577
|
+
: DOCS_INDEX_URL_DEFAULT ?? resolveDocsContentUrl(DOCS_URL_DEFAULT, null, "llms.txt");
|
|
1578
|
+
}
|
|
1449
1579
|
/** Fetch llms.txt / llms-full.txt / a prose page from the docs site,
|
|
1450
1580
|
* with a 1h cache in the config dir. Explicit command = explicit fetch;
|
|
1451
1581
|
* nothing here runs unless the user asked for docs. */
|
|
1452
1582
|
async function fetchDocs(pathOrFile) {
|
|
1453
1583
|
const base = docsSiteUrl();
|
|
1454
|
-
const url =
|
|
1455
|
-
? pathOrFile
|
|
1456
|
-
: base === null
|
|
1457
|
-
? null
|
|
1458
|
-
: base.replace(/\/+$/, "") + "/" + pathOrFile.replace(/^\/+/, "");
|
|
1584
|
+
const url = resolveDocsContentUrl(base, docsIndexUrl(), pathOrFile);
|
|
1459
1585
|
if (url === null)
|
|
1460
1586
|
return null;
|
|
1461
1587
|
const cacheFile = join(configDir(), "docs-cache", url.replace(/[^a-zA-Z0-9.]+/g, "_").slice(-120));
|
|
@@ -1466,13 +1592,9 @@ async function fetchDocs(pathOrFile) {
|
|
|
1466
1592
|
}
|
|
1467
1593
|
catch { /* not cached */ }
|
|
1468
1594
|
try {
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
signal: AbortSignal.timeout(10_000),
|
|
1472
|
-
});
|
|
1473
|
-
if (!response.ok)
|
|
1595
|
+
const text = await fetchDocsText(url);
|
|
1596
|
+
if (text === null)
|
|
1474
1597
|
return null;
|
|
1475
|
-
const text = await response.text();
|
|
1476
1598
|
mkdirSync(join(configDir(), "docs-cache"), { recursive: true, mode: 0o700 });
|
|
1477
1599
|
writeFileSync(cacheFile, text);
|
|
1478
1600
|
return text;
|
|
@@ -1618,31 +1740,7 @@ async function cmdDocs(parsed) {
|
|
|
1618
1740
|
.filter((match) => match.score > 0)
|
|
1619
1741
|
.sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
|
|
1620
1742
|
.map((match) => match.op);
|
|
1621
|
-
const
|
|
1622
|
-
const proseMatches = [];
|
|
1623
|
-
if (prose !== null) {
|
|
1624
|
-
let heading = "";
|
|
1625
|
-
const terms = searchTerms(term);
|
|
1626
|
-
for (const line of prose.split("\n")) {
|
|
1627
|
-
if (/^#{1,3} /.test(line))
|
|
1628
|
-
heading = line.replace(/^#+ /, "").trim();
|
|
1629
|
-
else {
|
|
1630
|
-
const lowerHeading = heading.toLowerCase();
|
|
1631
|
-
const lowerLine = line.toLowerCase();
|
|
1632
|
-
const matched = terms.filter((word) => lowerHeading.includes(word) || lowerLine.includes(word));
|
|
1633
|
-
if (matched.length > 0) {
|
|
1634
|
-
const allTerms = matched.length === terms.length;
|
|
1635
|
-
proseMatches.push({
|
|
1636
|
-
heading,
|
|
1637
|
-
excerpt: line.trim().slice(0, 160),
|
|
1638
|
-
score: matched.length * 10 + (allTerms ? 50 : 0) + (lowerHeading.includes(term.toLowerCase()) || lowerLine.includes(term.toLowerCase()) ? 25 : 0),
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
proseMatches.sort((a, b) => b.score - a.score || a.heading.localeCompare(b.heading) || a.excerpt.localeCompare(b.excerpt));
|
|
1644
|
-
}
|
|
1645
|
-
const docsStatus = docsSiteUrl() === null ? "not_configured" : prose === null ? "unavailable" : "ok";
|
|
1743
|
+
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1646
1744
|
if (jsonOutput) {
|
|
1647
1745
|
out({
|
|
1648
1746
|
schema_version: "1",
|
|
@@ -1654,7 +1752,7 @@ async function cmdDocs(parsed) {
|
|
|
1654
1752
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1655
1753
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1656
1754
|
})),
|
|
1657
|
-
guides: proseMatches.slice(0, 15).map((
|
|
1755
|
+
guides: proseMatches.slice(0, 15).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1658
1756
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1659
1757
|
guides_status: docsStatus,
|
|
1660
1758
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1671,7 +1769,7 @@ async function cmdDocs(parsed) {
|
|
|
1671
1769
|
if (proseMatches.length > 0) {
|
|
1672
1770
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1673
1771
|
for (const match of proseMatches.slice(0, 15))
|
|
1674
|
-
lines.push(" " +
|
|
1772
|
+
lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1675
1773
|
}
|
|
1676
1774
|
else if (docsStatus === "not_configured") {
|
|
1677
1775
|
lines.push(...(lines.length > 0 ? [""] : []), "(no docs site configured for guide search: '" + BIN + " config set docs-url <url>')");
|
|
@@ -1691,10 +1789,7 @@ async function cmdDocs(parsed) {
|
|
|
1691
1789
|
let target = page;
|
|
1692
1790
|
if (!/^https?:\/\//.test(target)) {
|
|
1693
1791
|
const index = await fetchDocs("llms.txt");
|
|
1694
|
-
|
|
1695
|
-
const hit = linked.find((u) => u.toLowerCase().includes(target.toLowerCase()));
|
|
1696
|
-
if (hit !== undefined)
|
|
1697
|
-
target = hit;
|
|
1792
|
+
target = docsReadTarget(index, docsSiteUrl(), docsIndexUrl(), target);
|
|
1698
1793
|
}
|
|
1699
1794
|
const text = await fetchDocs(target);
|
|
1700
1795
|
if (text === null) {
|
|
@@ -1708,6 +1803,9 @@ async function cmdDocs(parsed) {
|
|
|
1708
1803
|
if (sub !== undefined) {
|
|
1709
1804
|
const op = findOp(sub, parsed.positionals[2] ?? "");
|
|
1710
1805
|
if (!op) {
|
|
1806
|
+
const omitted = omittedCommand(sub, parsed.positionals[2]);
|
|
1807
|
+
if (omitted)
|
|
1808
|
+
failOmitted(omitted);
|
|
1711
1809
|
const list = OPS.filter((o) => o.command[0] === sub);
|
|
1712
1810
|
if (list.length === 0)
|
|
1713
1811
|
fail(2, "Unknown docs topic: " + sub + ". Run '" + BIN + " docs' for the overview.");
|
|
@@ -1759,182 +1857,9 @@ async function cmdDocs(parsed) {
|
|
|
1759
1857
|
process.stdout.write(lines.join("\n") + "\n");
|
|
1760
1858
|
await flushExit(0);
|
|
1761
1859
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
for (const key of ["type", "event", "event_type", "eventType", "name"]) {
|
|
1766
|
-
if (typeof parsed[key] === "string")
|
|
1767
|
-
return parsed[key];
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
catch { /* not JSON */ }
|
|
1771
|
-
return null;
|
|
1772
|
-
}
|
|
1773
|
-
async function cmdWebhooksFake(parsed) {
|
|
1774
|
-
void parsed;
|
|
1775
|
-
fail(2, "This API's spec declares no webhooks, so there is nothing to fake.");
|
|
1776
|
-
}
|
|
1777
|
-
async function cmdWebhooks(parsed) {
|
|
1778
|
-
const sub = parsed.positionals[1];
|
|
1779
|
-
if (parsed.help || sub === undefined) {
|
|
1780
|
-
const lines = [
|
|
1781
|
-
BIN + " webhooks — work with this API's webhook events",
|
|
1782
|
-
"",
|
|
1783
|
-
" " + BIN + " webhooks listen --forward-to localhost:3000/webhooks",
|
|
1784
|
-
" " + BIN + " webhooks listen --forward-to localhost:3000/hooks --events account.created,account.updated",
|
|
1785
|
-
" " + BIN + " webhooks fake [<event>] [--key whsec_...] [--forward-to <url>]",
|
|
1786
|
-
"",
|
|
1787
|
-
"listen mints a private relay URL to point the API's webhook settings",
|
|
1788
|
-
"at; events replay locally with their original headers, so signature",
|
|
1789
|
-
"verification keeps working. No tunnels, no exposed ports.",
|
|
1790
|
-
"fake sends (or prints) a signed sample event for local handler testing.",
|
|
1791
|
-
];
|
|
1792
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
1793
|
-
await flushExit(parsed.help ? 0 : 2);
|
|
1794
|
-
}
|
|
1795
|
-
if (sub === "fake") {
|
|
1796
|
-
await cmdWebhooksFake(parsed);
|
|
1797
|
-
}
|
|
1798
|
-
if (sub !== "listen")
|
|
1799
|
-
fail(2, "Unknown webhooks command: " + String(sub) + ". Try '" + BIN + " webhooks listen' or '" + BIN + " webhooks fake'.");
|
|
1800
|
-
if (!RELAY) {
|
|
1801
|
-
fail(2, "The webhook relay isn't enabled for this package. The API provider can enable it in their typeship console; the next regeneration bakes it in.");
|
|
1802
|
-
}
|
|
1803
|
-
const forwardRaw = parsed.flags.get("forward-to");
|
|
1804
|
-
if (typeof forwardRaw !== "string") {
|
|
1805
|
-
fail(2, "webhooks listen requires --forward-to <url>, e.g. --forward-to localhost:3000/webhooks");
|
|
1806
|
-
}
|
|
1807
|
-
const forwardTo = /^https?:\/\//.test(forwardRaw) ? forwardRaw : "http://" + forwardRaw;
|
|
1808
|
-
const eventsRaw = parsed.flags.get("events");
|
|
1809
|
-
const eventsFilter = typeof eventsRaw === "string"
|
|
1810
|
-
? eventsRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
1811
|
-
: null;
|
|
1812
|
-
let minted = null;
|
|
1813
|
-
try {
|
|
1814
|
-
const response = await fetch(RELAY.mintUrl, {
|
|
1815
|
-
method: "POST",
|
|
1816
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1817
|
-
body: JSON.stringify({ project: RELAY.project }),
|
|
1818
|
-
signal: AbortSignal.timeout(15_000),
|
|
1819
|
-
});
|
|
1820
|
-
minted = await response.json().catch(() => null);
|
|
1821
|
-
if (!response.ok || typeof minted?.ingest_url !== "string" || typeof minted.poll_url !== "string") {
|
|
1822
|
-
const detail = minted?.errors?.[0]?.message ?? "HTTP " + response.status;
|
|
1823
|
-
fail(1, "Couldn't start a relay session: " + detail);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
catch (e) {
|
|
1827
|
-
if (e instanceof ExitPending)
|
|
1828
|
-
throw e;
|
|
1829
|
-
fail(1, "Couldn't reach the relay: " + e.message);
|
|
1830
|
-
}
|
|
1831
|
-
process.stderr.write(paintErr("green", "Ready!") + " Forwarding relayed events to " + forwardTo + "\n");
|
|
1832
|
-
process.stderr.write("Point this API's webhook endpoint at: " + paintErr("cyan", minted.ingest_url) + " (^C to quit)\n");
|
|
1833
|
-
let cursor = 0;
|
|
1834
|
-
let pollFailures = 0;
|
|
1835
|
-
for (;;) {
|
|
1836
|
-
let payload;
|
|
1837
|
-
try {
|
|
1838
|
-
const response = await fetch(minted.poll_url + "?after=" + cursor + "&wait=1", {
|
|
1839
|
-
headers: { Accept: "application/json" },
|
|
1840
|
-
signal: AbortSignal.timeout(30_000),
|
|
1841
|
-
});
|
|
1842
|
-
if (response.status === 404)
|
|
1843
|
-
fail(1, "The relay session expired. Run '" + BIN + " webhooks listen' again.");
|
|
1844
|
-
if (!response.ok)
|
|
1845
|
-
throw new Error("HTTP " + response.status);
|
|
1846
|
-
payload = await response.json();
|
|
1847
|
-
pollFailures = 0;
|
|
1848
|
-
}
|
|
1849
|
-
catch (e) {
|
|
1850
|
-
if (e instanceof ExitPending)
|
|
1851
|
-
throw e;
|
|
1852
|
-
pollFailures++;
|
|
1853
|
-
if (pollFailures > 5)
|
|
1854
|
-
fail(1, "Lost the relay: " + e.message);
|
|
1855
|
-
await new Promise((resolve) => setTimeout(resolve, 2000 * pollFailures));
|
|
1856
|
-
continue;
|
|
1857
|
-
}
|
|
1858
|
-
for (const event of payload.events) {
|
|
1859
|
-
cursor = event.seq;
|
|
1860
|
-
const type = eventTypeOf(event.body);
|
|
1861
|
-
if (eventsFilter !== null && (type === null || !eventsFilter.includes(type)))
|
|
1862
|
-
continue;
|
|
1863
|
-
const started = Date.now();
|
|
1864
|
-
try {
|
|
1865
|
-
const forwarded = await fetch(forwardTo, {
|
|
1866
|
-
method: event.method,
|
|
1867
|
-
headers: event.headers,
|
|
1868
|
-
body: event.method === "GET" ? undefined : event.body,
|
|
1869
|
-
signal: AbortSignal.timeout(30_000),
|
|
1870
|
-
});
|
|
1871
|
-
process.stderr.write((forwarded.ok ? paintErr("green", String(forwarded.status)) : paintErr("yellow", String(forwarded.status))) +
|
|
1872
|
-
" " + (type ?? event.method) + " [" + event.seq + "] (" + (Date.now() - started) + "ms)\n");
|
|
1873
|
-
}
|
|
1874
|
-
catch (e) {
|
|
1875
|
-
process.stderr.write(paintErr("yellow", "unreachable") + " " + (type ?? event.method) + " [" + event.seq + "]: " + e.message + "\n");
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
if (payload.next > cursor)
|
|
1879
|
-
cursor = payload.next;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
async function cmdFeedback(parsed) {
|
|
1883
|
-
if (parsed.help) {
|
|
1884
|
-
process.stdout.write(BIN + " feedback — open the API provider's issue tracker with environment details prefilled\n");
|
|
1885
|
-
await flushExit(0);
|
|
1886
|
-
}
|
|
1887
|
-
if (!SUPPORT_URL) {
|
|
1888
|
-
fail(2, "No support URL is configured for this CLI. The API provider can set one in their typeship console.");
|
|
1889
|
-
}
|
|
1890
|
-
let target = SUPPORT_URL;
|
|
1891
|
-
if (/github\.com\/[^/]+\/[^/]+\/issues\/new/.test(target)) {
|
|
1892
|
-
const bodyLines = [
|
|
1893
|
-
"<!-- describe the problem or request above the line -->",
|
|
1894
|
-
"",
|
|
1895
|
-
"---",
|
|
1896
|
-
"- cli: " + BIN + " " + VERSION + " (api " + API_VERSION + ")",
|
|
1897
|
-
"- node: " + process.version,
|
|
1898
|
-
"- platform: " + process.platform + "/" + process.arch,
|
|
1899
|
-
];
|
|
1900
|
-
const sep = target.includes("?") ? "&" : "?";
|
|
1901
|
-
target += sep + "title=" + encodeURIComponent("[" + BIN + "] ") + "&body=" + encodeURIComponent(bodyLines.join("\n"));
|
|
1902
|
-
}
|
|
1903
|
-
if (!nonInteractive(parsed))
|
|
1904
|
-
process.stderr.write("Opening " + paintErr("cyan", SUPPORT_URL) + "\n");
|
|
1905
|
-
const opened = openInBrowser(target, parsed);
|
|
1906
|
-
if (opened.opened)
|
|
1907
|
-
out({ ok: true, opened: SUPPORT_URL });
|
|
1908
|
-
await flushExit(0);
|
|
1909
|
-
}
|
|
1910
|
-
/** Opt-in (console setting) once-a-day upgrade hint. Off by default:
|
|
1911
|
-
* generated code phones nobody unless the project owner chose this. The
|
|
1912
|
-
* notice prints the previous run's cached answer; at most once a day it
|
|
1913
|
-
* refreshes the cache first, with a 1.5s cap so a slow registry can't
|
|
1914
|
-
* hold a command hostage. */
|
|
1915
|
-
async function maybeUpdateNotice(commandWord, quiet) {
|
|
1916
|
-
if (!UPDATE_NOTICE || quiet)
|
|
1917
|
-
return;
|
|
1918
|
-
if (commandWord === undefined || ["upgrade", "version", "help", "login", "logout"].includes(commandWord))
|
|
1919
|
-
return;
|
|
1920
|
-
const cacheFile = join(configDir(), "update-check.json");
|
|
1921
|
-
let cache = {};
|
|
1922
|
-
try {
|
|
1923
|
-
cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
1924
|
-
}
|
|
1925
|
-
catch { /* no cache yet */ }
|
|
1926
|
-
if ((cache.checkedAt ?? 0) < Date.now() - 24 * 60 * 60 * 1000) {
|
|
1927
|
-
const latest = await latestVersion(1500);
|
|
1928
|
-
if (latest !== null) {
|
|
1929
|
-
cache = { checkedAt: Date.now(), latest };
|
|
1930
|
-
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
1931
|
-
writeFileSync(cacheFile, JSON.stringify(cache) + "\n");
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
if (cache.latest !== undefined && semverLess(VERSION, cache.latest)) {
|
|
1935
|
-
process.stderr.write(paintErr("yellow", "A newer " + BIN + " is available: " + VERSION + " -> " + cache.latest + ". Run '" + BIN + " upgrade'.") + "\n");
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1860
|
+
// ---------------------------------------------------------------------------
|
|
1861
|
+
// webhooks listen — forward relayed events to a local handler
|
|
1862
|
+
// ---------------------------------------------------------------------------
|
|
1938
1863
|
/** The command that fetches the next page: same positionals, the next page's query flags. */
|
|
1939
1864
|
function nextCommandFor(op, pathValues, next) {
|
|
1940
1865
|
const parts = [BIN, op.command[0], op.command[1], ...pathValues.map(shellQuote)];
|
|
@@ -2083,7 +2008,7 @@ function printRoot(stream = process.stdout) {
|
|
|
2083
2008
|
}
|
|
2084
2009
|
const width = termWidth();
|
|
2085
2010
|
const lines = [];
|
|
2086
|
-
lines.push(paintOut("bold", BIN) + ": " + "typeship" + " (v" + "0.
|
|
2011
|
+
lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.9.1");
|
|
2087
2012
|
lines.push("");
|
|
2088
2013
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
2089
2014
|
lines.push("");
|
|
@@ -2101,12 +2026,18 @@ function printRoot(stream = process.stdout) {
|
|
|
2101
2026
|
for (const more of wrapped.slice(1))
|
|
2102
2027
|
lines.push(" ".repeat(2 + col) + more);
|
|
2103
2028
|
}
|
|
2029
|
+
if (EXCLUDED_OPS > 0) {
|
|
2030
|
+
lines.push("");
|
|
2031
|
+
lines.push(...labeled(paintOut("yellow", "Plan limit:") + " ", "generated " + OPS.length + " of " + (OPS.length + EXCLUDED_OPS) + " operations", width, 14));
|
|
2032
|
+
lines.push(...labeled("Omitted: ", OMITTED_OPS.map((op) => op.command.join(" ") + " (" + op.httpMethod + " " + op.path + ")").join(", "), width, 14));
|
|
2033
|
+
lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
|
|
2034
|
+
}
|
|
2104
2035
|
lines.push("");
|
|
2105
|
-
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check bodies)" +
|
|
2036
|
+
const flagsText = "-v/--version, -h/--help, --debug, --non-interactive, --color on|off|auto, --base-url <url>, --profile <name>, --credentials @<file>|-, --data '<json>', --fields <a,b.c>, --all (paginated lists), --validate (schema-check bodies)" +
|
|
2106
2037
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
2107
2038
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
2108
2039
|
lines.push(...labeled("Credential env vars: ", [
|
|
2109
|
-
...AUTH_SCALARS.map((a) => a.env),
|
|
2040
|
+
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
2110
2041
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
2111
2042
|
].join(", ") || "none", width, 21));
|
|
2112
2043
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
@@ -2118,9 +2049,9 @@ function printRoot(stream = process.stdout) {
|
|
|
2118
2049
|
lines.push("Webhooks: " + BIN + " webhooks listen --forward-to <url> (local event forwarding)");
|
|
2119
2050
|
if (SUPPORT_URL)
|
|
2120
2051
|
lines.push("Feedback: " + BIN + " feedback (opens the provider's issue tracker)");
|
|
2121
|
-
if (
|
|
2052
|
+
if (MCP_EXCLUDED_OPS > 0 && HAS_MCP) {
|
|
2122
2053
|
lines.push("");
|
|
2123
|
-
lines.push("Note: " +
|
|
2054
|
+
lines.push("Note: " + MCP_EXCLUDED_OPS + " generated operation(s) with uploads or event streams are CLI/SDK-only, not MCP tools.");
|
|
2124
2055
|
}
|
|
2125
2056
|
lines.push("");
|
|
2126
2057
|
lines.push("Run '" + BIN + " <resource>' for a resource's commands, '" + BIN + " <resource> <command> --help' for flags.");
|
|
@@ -2151,6 +2082,8 @@ function printResource(resource, stream = process.stdout) {
|
|
|
2151
2082
|
/** Flags the CLI itself adds to an API command, as [flag, description] rows. */
|
|
2152
2083
|
function commandExtras(op) {
|
|
2153
2084
|
const extras = [];
|
|
2085
|
+
if (op.auth !== "none" && Object.keys(NAMED_SCHEMES).length)
|
|
2086
|
+
extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
|
|
2154
2087
|
if (op.hasBody && op.bodyKind === "binary")
|
|
2155
2088
|
extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
2156
2089
|
else if (op.hasBody)
|
|
@@ -2202,7 +2135,7 @@ function exampleLine(op) {
|
|
|
2202
2135
|
}
|
|
2203
2136
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2204
2137
|
function authNote(op) {
|
|
2205
|
-
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null ||
|
|
2138
|
+
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2206
2139
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2207
2140
|
}
|
|
2208
2141
|
function printOp(op) {
|
|
@@ -2379,38 +2312,89 @@ function coerce(spec, raw, repeated) {
|
|
|
2379
2312
|
}
|
|
2380
2313
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2381
2314
|
let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
2315
|
+
function validateCredentialsInput(flags) {
|
|
2316
|
+
const input = flags.get("credentials");
|
|
2317
|
+
if (BASIC && flags.has("username") !== flags.has("password"))
|
|
2318
|
+
fail(2, "Supply --username and --password together, or use a complete named Basic credential.");
|
|
2319
|
+
if (input === undefined)
|
|
2320
|
+
return;
|
|
2321
|
+
if (typeof input !== "string" || (input !== "-" && (!input.startsWith("@") || input.length < 2)))
|
|
2322
|
+
fail(2, "--credentials expects @<JSON-file> or - to read JSON from stdin.");
|
|
2323
|
+
if (input === "-" && (flags.get("data") === "-" || flags.get("file") === "-" || flags.get("with-token") === true))
|
|
2324
|
+
fail(2, "Stdin cannot supply both named credentials and another input. Use a credential file instead.");
|
|
2325
|
+
}
|
|
2326
|
+
let namedFlagCache;
|
|
2327
|
+
function flagCredentials(flags) {
|
|
2328
|
+
if (namedFlagCache)
|
|
2329
|
+
return namedFlagCache;
|
|
2330
|
+
validateCredentialsInput(flags);
|
|
2331
|
+
const input = flags.get("credentials");
|
|
2332
|
+
if (input === undefined)
|
|
2333
|
+
return {};
|
|
2334
|
+
try {
|
|
2335
|
+
const named = readNamedCredentialsFile(input, NAMED_SCHEMES);
|
|
2336
|
+
if (!Object.keys(named).length)
|
|
2337
|
+
fail(2, "The named credential file must contain at least one security scheme.");
|
|
2338
|
+
return namedFlagCache = named;
|
|
2339
|
+
}
|
|
2340
|
+
catch (error) {
|
|
2341
|
+
fail(2, error.message);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
function environmentCredentials() {
|
|
2345
|
+
if (BASIC && (process.env[BASIC.envUser] !== undefined) !== (process.env[BASIC.envPass] !== undefined))
|
|
2346
|
+
fail(2, "Supply both Basic-auth environment variables together, or use a complete named Basic credential.");
|
|
2347
|
+
const value = process.env["TYPESHIP_CREDENTIALS"];
|
|
2348
|
+
return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
|
|
2349
|
+
}
|
|
2382
2350
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2383
2351
|
function credentialSource(flags) {
|
|
2352
|
+
if (Object.keys(flagCredentials(flags)).length)
|
|
2353
|
+
return "flags";
|
|
2384
2354
|
if (AUTH_SCALARS.some((a) => typeof flags.get(a.flag) === "string"))
|
|
2385
2355
|
return "flags";
|
|
2386
2356
|
if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string")
|
|
2387
2357
|
return "flags";
|
|
2358
|
+
if (Object.keys(environmentCredentials()).length)
|
|
2359
|
+
return "env:TYPESHIP_CREDENTIALS";
|
|
2388
2360
|
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
2389
2361
|
if (envScalar)
|
|
2390
2362
|
return "env:" + envScalar.env;
|
|
2391
2363
|
if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
|
|
2392
2364
|
return "env:" + BASIC.envUser;
|
|
2393
2365
|
const stored = readCreds();
|
|
2394
|
-
if (stored && (stored.scalars || stored.basic || stored.oauth))
|
|
2366
|
+
if (stored && (stored.scalars || stored.basic || stored.oauth || stored.named))
|
|
2395
2367
|
return "login";
|
|
2396
2368
|
return null;
|
|
2397
2369
|
}
|
|
2398
2370
|
/** Base URL resolution: --base-url > env > config base-url > config environment > spec default. */
|
|
2399
|
-
function resolveBaseUrl(flags) {
|
|
2400
|
-
const config = readConfig();
|
|
2371
|
+
function resolveBaseUrl(flags, config = readConfig()) {
|
|
2401
2372
|
return (typeof flags.get("base-url") === "string" ? flags.get("base-url") : undefined)
|
|
2402
2373
|
?? process.env["TYPESHIP_BASE_URL"]
|
|
2403
2374
|
?? config.baseUrl
|
|
2404
2375
|
?? (config.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
|
|
2405
2376
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2406
2377
|
}
|
|
2407
|
-
async function makeClient(flags) {
|
|
2408
|
-
const
|
|
2409
|
-
const
|
|
2410
|
-
|
|
2378
|
+
async function makeClient(flags, op, candidate, forIdentity = false) {
|
|
2379
|
+
const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
|
|
2380
|
+
const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
|
|
2381
|
+
if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined))
|
|
2382
|
+
explicitOptions.add("basicAuth");
|
|
2383
|
+
namedCredentialAvailability(NAMED_SCHEMES, explicitOptions, { ...envNamed, ...flagNamed });
|
|
2384
|
+
const explicitCredentials = op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => explicitOptions.has(option)));
|
|
2385
|
+
// A complete explicit alternative for this endpoint does not unlock or mix
|
|
2386
|
+
// in saved credentials for another API, account, or authentication method.
|
|
2387
|
+
const stored = candidate ?? (op.auth === "none" || explicitCredentials ? null : readCreds());
|
|
2388
|
+
const config = readConfig();
|
|
2389
|
+
const baseUrl = resolveBaseUrl(flags, config);
|
|
2411
2390
|
if (baseUrl === undefined)
|
|
2412
2391
|
fail(2, "No base URL. Pass --base-url, set TYPESHIP_BASE_URL, or run '" + BIN + " config set base-url <url>'.");
|
|
2413
|
-
|
|
2392
|
+
if (stored) {
|
|
2393
|
+
assertCredentialDestination(stored, { apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name });
|
|
2394
|
+
if (!forIdentity)
|
|
2395
|
+
assertStoredIdentity(stored, identityConfiguration());
|
|
2396
|
+
}
|
|
2397
|
+
const options = { baseUrl };
|
|
2414
2398
|
for (const a of AUTH_SCALARS) {
|
|
2415
2399
|
const v = (typeof flags.get(a.flag) === "string" ? flags.get(a.flag) : undefined)
|
|
2416
2400
|
?? process.env[a.env] ?? stored?.scalars?.[a.option];
|
|
@@ -2425,10 +2409,28 @@ async function makeClient(flags) {
|
|
|
2425
2409
|
if (username !== undefined && password !== undefined)
|
|
2426
2410
|
options.basicAuth = { username, password };
|
|
2427
2411
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
if (
|
|
2431
|
-
|
|
2412
|
+
const envOptions = {}, flagOptions = {};
|
|
2413
|
+
for (const a of AUTH_SCALARS) {
|
|
2414
|
+
if (process.env[a.env] !== undefined)
|
|
2415
|
+
envOptions[a.option] = process.env[a.env];
|
|
2416
|
+
if (typeof flags.get(a.flag) === "string")
|
|
2417
|
+
flagOptions[a.option] = flags.get(a.flag);
|
|
2418
|
+
}
|
|
2419
|
+
if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
|
|
2420
|
+
envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
2421
|
+
if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string")
|
|
2422
|
+
flagOptions.basicAuth = { username: flags.get("username"), password: flags.get("password") };
|
|
2423
|
+
options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
|
|
2424
|
+
{ named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
|
|
2425
|
+
{ named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
|
|
2426
|
+
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2427
|
+
]);
|
|
2428
|
+
if (stored?.oauth && (forIdentity || options.bearerToken === undefined)) {
|
|
2429
|
+
const sessionId = stored.oauth.sessionId;
|
|
2430
|
+
options.bearerToken = forIdentity ? stored.oauth.accessToken : () => oauthSessionToken(credentialStore(), {
|
|
2431
|
+
...sessionConfiguration(baseUrl, config),
|
|
2432
|
+
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken) => verifyClientIdentity((values) => new TypeshipClient(values), { ...options, bearerToken: accessToken, credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ named: options.credentials }, { options: { bearerToken: accessToken } }]) }, WHOAMI, IDENTITY_POLICY) } : {}),
|
|
2433
|
+
}, sessionId, OAUTH_TOKEN_PARAMS);
|
|
2432
2434
|
}
|
|
2433
2435
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2434
2436
|
options.debug = (event) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
@@ -2441,7 +2443,7 @@ async function makeClient(flags) {
|
|
|
2441
2443
|
if (value !== undefined)
|
|
2442
2444
|
options[g.option] = value;
|
|
2443
2445
|
}
|
|
2444
|
-
LAST_CLIENT_HAD_CREDENTIAL = AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2446
|
+
LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2445
2447
|
// Who is calling: the CLI, under which agent harness, and whether an
|
|
2446
2448
|
// agent is driving. "agent" means a harness was detected or the caller
|
|
2447
2449
|
// said so (--mode agent / env); a bare non-TTY run (CI, a pipeline) is
|
|
@@ -2455,6 +2457,11 @@ async function makeClient(flags) {
|
|
|
2455
2457
|
...options.defaultHeaders,
|
|
2456
2458
|
"User-Agent": PKG_NAME + "-cli/" + VERSION + " (typeship" + (harness ? "; harness=" + harness : "") + caller + ")",
|
|
2457
2459
|
};
|
|
2460
|
+
if (forIdentity) {
|
|
2461
|
+
options.fetch = identityFetch(baseUrl);
|
|
2462
|
+
options.maxRetries = 0;
|
|
2463
|
+
options.timeoutMs = 10_000;
|
|
2464
|
+
}
|
|
2458
2465
|
return new TypeshipClient(options);
|
|
2459
2466
|
}
|
|
2460
2467
|
function editDistance(a, b) {
|
|
@@ -2483,10 +2490,31 @@ function didYouMean(input, candidates) {
|
|
|
2483
2490
|
}
|
|
2484
2491
|
return best;
|
|
2485
2492
|
}
|
|
2486
|
-
|
|
2493
|
+
function omittedCommand(resource, method) {
|
|
2494
|
+
return OMITTED_OPS.find((op) => op.command[0] === resource &&
|
|
2495
|
+
(method === undefined || op.command[1] === method || op.commandAlias === method));
|
|
2496
|
+
}
|
|
2497
|
+
function failOmitted(op) {
|
|
2498
|
+
return failWith({
|
|
2499
|
+
status: "action_required",
|
|
2500
|
+
code: "PLAN_LIMIT",
|
|
2501
|
+
message: "The command '" + BIN + " " + op.command.join(" ") + "' exists in the API Definition but was omitted from this generated package by its plan limit.",
|
|
2502
|
+
detail: { operation: op.tool, method: op.httpMethod, path: op.path, generated_operations: OPS.length, total_operations: OPS.length + EXCLUDED_OPS },
|
|
2503
|
+
nextSteps: ["Upgrade at https://typeship.dev/pricing and regenerate the package without the operation cap.", "Do not invent or retry an omitted command against this generated package."],
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs", "upgrade", "completion", "help", "version", "init", "agent-guide", "auth", "doctor"];
|
|
2487
2507
|
async function main() {
|
|
2488
2508
|
const argv = process.argv.slice(2);
|
|
2489
2509
|
const parsed = parseArgv(argv);
|
|
2510
|
+
if (!parsed.help)
|
|
2511
|
+
validateCredentialsInput(parsed.flags);
|
|
2512
|
+
const profileFlag = parsed.flags.get("profile");
|
|
2513
|
+
if (profileFlag !== undefined && typeof profileFlag !== "string")
|
|
2514
|
+
fail(2, "--profile requires a profile name.");
|
|
2515
|
+
PROFILE = resolveProfile(configRoot(), { flag: profileFlag, environment: process.env["TYPESHIP_PROFILE"], allowMissing: ["login", "config", "auth", "init", "help"].includes(parsed.positionals[0] ?? "") || parsed.help });
|
|
2516
|
+
if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? ""))
|
|
2517
|
+
expectedLoginIdentity(parsed.flags);
|
|
2490
2518
|
if (parsed.positionals[0] === "help") {
|
|
2491
2519
|
// help --json: the command surface as data (agents read this once).
|
|
2492
2520
|
if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") {
|
|
@@ -2511,9 +2539,8 @@ async function main() {
|
|
|
2511
2539
|
COLOR_ERR = colorEnabled(process.stderr, parsed);
|
|
2512
2540
|
// Prose errors for a person: stderr is a terminal (or --mode human says
|
|
2513
2541
|
// to behave as if), and nothing asked for JSON.
|
|
2514
|
-
const forcedHuman = parsed.flags.get("mode") === "human" ||
|
|
2542
|
+
const forcedHuman = parsed.flags.get("mode") === "human" || process.env["TYPESHIP_MODE"] === "human";
|
|
2515
2543
|
HUMAN_ERRORS = (process.stderr.isTTY === true || forcedHuman) && !isAgentMode(parsed) && parsed.flags.get("format") !== "json" && parsed.flags.get("json") !== true;
|
|
2516
|
-
await maybeUpdateNotice(resourceCmd, explicitNonInteractive(parsed));
|
|
2517
2544
|
if (resourceCmd === "init") {
|
|
2518
2545
|
await cmdInit(parsed);
|
|
2519
2546
|
}
|
|
@@ -2529,12 +2556,6 @@ async function main() {
|
|
|
2529
2556
|
if (resourceCmd === "upgrade") {
|
|
2530
2557
|
await cmdUpgrade(parsed);
|
|
2531
2558
|
}
|
|
2532
|
-
if (resourceCmd === "webhooks") {
|
|
2533
|
-
await cmdWebhooks(parsed);
|
|
2534
|
-
}
|
|
2535
|
-
if (resourceCmd === "feedback") {
|
|
2536
|
-
await cmdFeedback(parsed);
|
|
2537
|
-
}
|
|
2538
2559
|
if (resourceCmd === "docs") {
|
|
2539
2560
|
await cmdDocs(parsed);
|
|
2540
2561
|
}
|
|
@@ -2552,10 +2573,10 @@ async function main() {
|
|
|
2552
2573
|
}
|
|
2553
2574
|
if (resourceCmd === "logout") {
|
|
2554
2575
|
if (parsed.help) {
|
|
2555
|
-
process.stdout.write(BIN + " logout — remove " + credsPath() + "\n");
|
|
2576
|
+
process.stdout.write(BIN + " logout — remove " + credsPath() + "\n --local remove the local session without unlocking OS storage or revoking tokens\n");
|
|
2556
2577
|
await flushExit(0);
|
|
2557
2578
|
}
|
|
2558
|
-
await cmdLogout();
|
|
2579
|
+
await cmdLogout(parsed);
|
|
2559
2580
|
}
|
|
2560
2581
|
if (resourceCmd === "whoami") {
|
|
2561
2582
|
if (parsed.help) {
|
|
@@ -2574,6 +2595,9 @@ async function main() {
|
|
|
2574
2595
|
}
|
|
2575
2596
|
const resourceExists = OPS.some((o) => o.command[0] === resourceCmd);
|
|
2576
2597
|
if (!resourceExists) {
|
|
2598
|
+
const omitted = omittedCommand(resourceCmd, methodCmd);
|
|
2599
|
+
if (omitted)
|
|
2600
|
+
failOmitted(omitted);
|
|
2577
2601
|
const suggestion = didYouMean(resourceCmd, [...new Set(OPS.map((o) => o.command[0])), ...BUILTIN_COMMANDS]);
|
|
2578
2602
|
fail(2, "Unknown command: " + resourceCmd + "." + (suggestion ? " Did you mean '" + BIN + " " + suggestion + "'?" : ""), undefined, ["Run '" + BIN + " --help' for the commands."]);
|
|
2579
2603
|
}
|
|
@@ -2583,6 +2607,9 @@ async function main() {
|
|
|
2583
2607
|
}
|
|
2584
2608
|
const op = findOp(resourceCmd, methodCmd);
|
|
2585
2609
|
if (!op) {
|
|
2610
|
+
const omitted = omittedCommand(resourceCmd, methodCmd);
|
|
2611
|
+
if (omitted)
|
|
2612
|
+
failOmitted(omitted);
|
|
2586
2613
|
const suggestion = didYouMean(methodCmd, OPS.filter((o) => o.command[0] === resourceCmd).map((o) => o.command[1]));
|
|
2587
2614
|
fail(2, "Unknown command: " + resourceCmd + " " + methodCmd + "." + (suggestion ? " Did you mean '" + BIN + " " + resourceCmd + " " + suggestion + "'?" : ""), undefined, ["Run '" + BIN + " " + resourceCmd + "' for its commands."]);
|
|
2588
2615
|
}
|
|
@@ -2622,7 +2649,7 @@ async function main() {
|
|
|
2622
2649
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2623
2650
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2624
2651
|
// check can be exact.
|
|
2625
|
-
const RESERVED_FLAGS = new Set(["data", "all", "select", "base-url", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
|
|
2652
|
+
const RESERVED_FLAGS = new Set(["data", "credentials", "all", "select", "base-url", "profile", "debug", "validate", "non-interactive", "color", "version", "help", "yes", "force", "mode", "format", "json", "out", "fields", ...AUTH_SCALARS.map((a) => a.flag), ...(BASIC ? ["username", "password"] : []), ...GLOBALS.map((g) => g.flag)]);
|
|
2626
2653
|
for (const spec of op.params) {
|
|
2627
2654
|
if (spec.kind === "path")
|
|
2628
2655
|
continue;
|
|
@@ -2678,7 +2705,7 @@ async function main() {
|
|
|
2678
2705
|
}
|
|
2679
2706
|
// The spec says this operation needs a credential and none resolved:
|
|
2680
2707
|
// say so now, locally, instead of sending a request to learn it.
|
|
2681
|
-
if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC ||
|
|
2708
|
+
if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC || HAS_OAUTH_LOGIN) && credentialSource(parsed.flags) === null) {
|
|
2682
2709
|
failWith({
|
|
2683
2710
|
status: "action_required",
|
|
2684
2711
|
code: "NO_AUTH",
|
|
@@ -2709,7 +2736,7 @@ async function main() {
|
|
|
2709
2736
|
if (answer !== "y" && answer !== "yes")
|
|
2710
2737
|
failWith({ status: "action_required", code: "CONFIRMATION_REQUIRED", message: "Cancelled.", nextSteps: ["Run again with --force to skip the prompt: " + rerun] });
|
|
2711
2738
|
}
|
|
2712
|
-
const client = await makeClient(parsed.flags);
|
|
2739
|
+
const client = await makeClient(parsed.flags, op);
|
|
2713
2740
|
const selectValue = typeof parsed.flags.get("select") === "string" ? parsed.flags.get("select") : undefined;
|
|
2714
2741
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2715
2742
|
const target = client[op.resource];
|
|
@@ -2758,6 +2785,7 @@ async function main() {
|
|
|
2758
2785
|
items: project(page.items),
|
|
2759
2786
|
hasMore: next !== null,
|
|
2760
2787
|
...(next !== null ? { nextPage: next, nextCommand: nextCommandFor(op, pathValues, next) } : {}),
|
|
2788
|
+
...(page.response.requestId ? { request_id: page.response.requestId } : {}),
|
|
2761
2789
|
});
|
|
2762
2790
|
}
|
|
2763
2791
|
else if (FIELDS !== null && collectionField !== null && result.data !== null && typeof result.data === "object" && !Array.isArray(result.data) && Array.isArray(result.data[collectionField])) {
|
|
@@ -2789,16 +2817,6 @@ function errorMessage(error) {
|
|
|
2789
2817
|
}
|
|
2790
2818
|
return base;
|
|
2791
2819
|
}
|
|
2792
|
-
function serializeError(error) {
|
|
2793
|
-
if (error && typeof error === "object" && "violations" in error) {
|
|
2794
|
-
return { violations: error.violations };
|
|
2795
|
-
}
|
|
2796
|
-
if (error && typeof error === "object" && "status" in error) {
|
|
2797
|
-
const e = error;
|
|
2798
|
-
return { status: e.status, body: e.body };
|
|
2799
|
-
}
|
|
2800
|
-
return undefined;
|
|
2801
|
-
}
|
|
2802
2820
|
main().catch((e) => {
|
|
2803
2821
|
if (e instanceof ExitPending)
|
|
2804
2822
|
return;
|