@typeship-ax/cli 0.8.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 +1875 -684
- package/api.md +352 -500
- 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.js +522 -355
- 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 +17 -2
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +78 -17
- 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/docs.d.ts +25 -0
- package/dist/docs.d.ts.map +1 -1
- package/dist/docs.js +144 -0
- package/dist/errors.d.ts +18 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +24 -14
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- 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 +14 -1
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +30 -30
- 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 +2 -2
- package/dist/resources/account.d.ts.map +1 -1
- package/dist/resources/account.js +1 -0
- package/dist/resources/api-keys.d.ts +3 -3
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +2 -0
- package/dist/resources/definition-revisions.d.ts +5 -5
- package/dist/resources/definition-revisions.d.ts.map +1 -1
- package/dist/resources/definition-revisions.js +4 -0
- package/dist/resources/definitions.d.ts +15 -4
- package/dist/resources/definitions.d.ts.map +1 -1
- package/dist/resources/definitions.js +11 -2
- package/dist/resources/generate.d.ts +14 -3
- package/dist/resources/generate.d.ts.map +1 -1
- package/dist/resources/generate.js +10 -2
- package/dist/resources/generations.d.ts +3 -3
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +2 -0
- package/dist/resources/projects.d.ts +56 -20
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +40 -4
- package/dist/resources/targets.d.ts +20 -9
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +14 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +38 -22
- package/dist/types.d.ts +385 -119
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/package.json +2 -1
- package/src/api-identity.ts +98 -0
- package/src/auth-profiles.ts +114 -0
- package/src/cli.ts +444 -332
- package/src/console-login-check.ts +88 -0
- package/src/console-login-contract.ts +65 -0
- package/src/core/http.ts +88 -19
- package/src/credential-storage.ts +183 -0
- package/src/docs.ts +138 -0
- package/src/errors.ts +26 -15
- package/src/index.ts +29 -4
- 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 +44 -31
- package/src/polling-login.ts +165 -0
- package/src/resources/account.ts +3 -0
- package/src/resources/api-keys.ts +5 -0
- package/src/resources/definition-revisions.ts +9 -0
- package/src/resources/definitions.ts +25 -0
- package/src/resources/generate.ts +23 -0
- package/src/resources/generations.ts +5 -0
- package/src/resources/projects.ts +95 -7
- package/src/resources/targets.ts +32 -0
- package/src/schemas.ts +38 -22
- package/src/types.ts +404 -119
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,17 @@
|
|
|
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";
|
|
@@ -14,18 +23,21 @@ import { TypeshipClient, formatDebugEvent } from "./index.js";
|
|
|
14
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";
|
|
17
|
-
import { fetchDocsText, resolveDocsContentUrl } from "./docs.js";
|
|
26
|
+
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
18
27
|
const BIN = "typeship";
|
|
19
28
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
29
|
+
const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
|
|
20
30
|
const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
|
|
21
31
|
const BASIC = null;
|
|
22
32
|
/** Operations omitted from the generated package by its plan cap. */
|
|
23
33
|
const EXCLUDED_OPS = 0;
|
|
24
34
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
25
35
|
const MCP_EXCLUDED_OPS = 0;
|
|
26
|
-
const VERSION = "0.
|
|
36
|
+
const VERSION = "0.9.1";
|
|
27
37
|
const API_VERSION = "1.0.0";
|
|
28
38
|
const SPEC_FORMAT = "openapi";
|
|
39
|
+
const IDENTITY_POLICY = {};
|
|
40
|
+
let LOGIN_IDENTITY;
|
|
29
41
|
const WHOAMI = { "resource": "account", "method": "retrieve" };
|
|
30
42
|
const ENVIRONMENTS = {};
|
|
31
43
|
const HAS_MCP = false;
|
|
@@ -37,6 +49,15 @@ const DOCS_INDEX_URL_DEFAULT = null;
|
|
|
37
49
|
const RELAY = null;
|
|
38
50
|
const SUPPORT_URL = null;
|
|
39
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;
|
|
40
61
|
const OAUTH_CLIENT_ID = null;
|
|
41
62
|
const OAUTH_SCOPES = [];
|
|
42
63
|
const OAUTH_TOKEN_PARAMS = {};
|
|
@@ -53,11 +74,12 @@ const API_TITLE = "typeship";
|
|
|
53
74
|
* is known. */
|
|
54
75
|
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
|
|
55
76
|
const BUILTIN_BOOLEAN_FLAGS = {
|
|
56
|
-
login: ["with-token", "no-browser"],
|
|
77
|
+
login: ["with-token", "no-browser", "device"],
|
|
78
|
+
logout: ["local"],
|
|
57
79
|
upgrade: ["check"],
|
|
58
80
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
59
81
|
docs: ["web", "schema"],
|
|
60
|
-
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md"],
|
|
82
|
+
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
61
83
|
auth: ["live"],
|
|
62
84
|
doctor: [],
|
|
63
85
|
};
|
|
@@ -305,24 +327,92 @@ function fail(code, message, extra, nextSteps) {
|
|
|
305
327
|
function failApi(error, hadCredential) {
|
|
306
328
|
return failWith(classifyApiError(error, { bin: BIN, hadCredential, docsUrl: DOCS_URL_DEFAULT }));
|
|
307
329
|
}
|
|
308
|
-
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// credentials — written by `login`, cleared by `logout`
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
function configRoot() {
|
|
309
334
|
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
310
335
|
return join(base, BIN);
|
|
311
336
|
}
|
|
337
|
+
let PROFILE;
|
|
338
|
+
function configDir() { return PROFILE.directory; }
|
|
312
339
|
function credsPath() {
|
|
313
|
-
return
|
|
340
|
+
return credentialStore().path;
|
|
314
341
|
}
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
|
|
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;
|
|
318
367
|
}
|
|
319
|
-
|
|
320
|
-
|
|
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;
|
|
321
373
|
}
|
|
374
|
+
return Object.keys(expected).length ? expected : undefined;
|
|
322
375
|
}
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
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 }));
|
|
326
416
|
}
|
|
327
417
|
function readStdin() {
|
|
328
418
|
return new Promise((resolve) => {
|
|
@@ -384,130 +474,48 @@ function promptHidden(promptText) {
|
|
|
384
474
|
stdin.on("data", onData);
|
|
385
475
|
});
|
|
386
476
|
}
|
|
387
|
-
async function
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
});
|
|
393
|
-
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);
|
|
394
482
|
try {
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
async function deviceLogin(clientId,
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
fail(1, "The authorization server does not advertise a device flow. Run '" + BIN + " login' with a pasted credential instead.");
|
|
425
|
-
}
|
|
426
|
-
const start = await oauthForm(deviceEndpoint, {
|
|
427
|
-
client_id: clientId,
|
|
428
|
-
...(OAUTH_SCOPES.length > 0 ? { scope: OAUTH_SCOPES.join(" ") } : {}),
|
|
429
|
-
...OAUTH_TOKEN_PARAMS,
|
|
430
|
-
});
|
|
431
|
-
const startBody = start.body;
|
|
432
|
-
if (start.status !== 200 || !startBody?.device_code) {
|
|
433
|
-
fail(1, "Device authorization failed (HTTP " + start.status + ").", start.body);
|
|
434
|
-
}
|
|
435
|
-
const uri = startBody.verification_uri_complete ?? startBody.verification_uri;
|
|
436
|
-
process.stderr.write("Open " + paintErr("cyan", String(uri)) + " and enter code: " + paintErr("bold", String(startBody.user_code)) + "\n");
|
|
437
|
-
// Under an agent the same facts also go out as one JSON line (stderr,
|
|
438
|
-
// so stdout stays the single result document), for the agent to hand
|
|
439
|
-
// the URL and code to the user while this polls.
|
|
440
|
-
if (agent)
|
|
441
|
-
process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: uri, user_code: startBody.user_code, expires_in: startBody.expires_in ?? 900 }) + "\n");
|
|
442
|
-
let intervalMs = (startBody.interval ?? 5) * 1000;
|
|
443
|
-
const deadline = Date.now() + (startBody.expires_in ?? 900) * 1000;
|
|
444
|
-
while (Date.now() < deadline) {
|
|
445
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
446
|
-
const poll = await oauthForm(tokenEndpoint, {
|
|
447
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
448
|
-
device_code: startBody.device_code,
|
|
449
|
-
client_id: clientId,
|
|
450
|
-
});
|
|
451
|
-
const tokenBody = poll.body;
|
|
452
|
-
if (poll.status === 200 && tokenBody?.access_token) {
|
|
453
|
-
writeCreds({
|
|
454
|
-
...(readCreds() ?? {}),
|
|
455
|
-
oauth: {
|
|
456
|
-
accessToken: tokenBody.access_token,
|
|
457
|
-
refreshToken: tokenBody.refresh_token,
|
|
458
|
-
expiresAt: tokenBody.expires_in ? Date.now() + tokenBody.expires_in * 1000 : undefined,
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
process.stderr.write(paintErr("green", "Logged in.") + "\n");
|
|
462
|
-
out({ ok: true, method: "device", credentials: credsPath() });
|
|
463
|
-
await flushExit(0);
|
|
464
|
-
}
|
|
465
|
-
const errorCode = tokenBody?.error;
|
|
466
|
-
if (errorCode === "authorization_pending")
|
|
467
|
-
continue;
|
|
468
|
-
if (errorCode === "slow_down") {
|
|
469
|
-
intervalMs += 5000;
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
fail(1, "Device login failed: " + (errorCode ?? "HTTP " + poll.status), poll.body);
|
|
473
|
-
}
|
|
474
|
-
fail(1, "Device login timed out before the code was entered.");
|
|
475
|
-
}
|
|
476
|
-
/** Stored OAuth access token, refreshed through the token URL when expired.
|
|
477
|
-
* A failed refresh returns the stale token; the API's 401 explains better
|
|
478
|
-
* than a local guess. */
|
|
479
|
-
async function refreshedOauthToken(stored) {
|
|
480
|
-
const oauth = stored.oauth;
|
|
481
|
-
if (!oauth)
|
|
482
|
-
return undefined;
|
|
483
|
-
const expired = oauth.expiresAt !== undefined && Date.now() > oauth.expiresAt - 60_000;
|
|
484
|
-
if (!expired || !oauth.refreshToken || !OAUTH_TOKEN_URL)
|
|
485
|
-
return oauth.accessToken;
|
|
486
|
-
const clientId = process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
487
|
-
const result = await oauthForm(OAUTH_TOKEN_URL, {
|
|
488
|
-
grant_type: "refresh_token",
|
|
489
|
-
refresh_token: oauth.refreshToken,
|
|
490
|
-
...(clientId ? { client_id: clientId } : {}),
|
|
491
|
-
});
|
|
492
|
-
const body = result.body;
|
|
493
|
-
if (result.status === 200 && body?.access_token) {
|
|
494
|
-
const next = {
|
|
495
|
-
accessToken: body.access_token,
|
|
496
|
-
refreshToken: body.refresh_token ?? oauth.refreshToken,
|
|
497
|
-
expiresAt: body.expires_in ? Date.now() + body.expires_in * 1000 : undefined,
|
|
498
|
-
};
|
|
499
|
-
writeCreds({ ...stored, oauth: next });
|
|
500
|
-
return next.accessToken;
|
|
501
|
-
}
|
|
502
|
-
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);
|
|
503
512
|
}
|
|
504
|
-
async function storePastedToken(
|
|
513
|
+
async function storePastedToken(token, flags) {
|
|
505
514
|
const first = AUTH_SCALARS[0];
|
|
506
515
|
if (!first)
|
|
507
516
|
fail(2, "This API declares no credential the CLI can store. Use --username/--password if it uses basic auth.");
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
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() });
|
|
511
519
|
await flushExit(0);
|
|
512
520
|
}
|
|
513
521
|
/**
|
|
@@ -517,97 +525,139 @@ async function storePastedToken(stored, token) {
|
|
|
517
525
|
* until the API hands back a key minted for this CLI, store it. The key
|
|
518
526
|
* never crosses the chat: the agent relays a URL, the person clicks once.
|
|
519
527
|
*/
|
|
520
|
-
/**
|
|
521
|
-
*
|
|
528
|
+
/** A same-origin API override uses that deployment's approval route. A
|
|
529
|
+
* separately hosted identity service retains its explicitly configured URL. */
|
|
522
530
|
function cliAuthUrl(flags) {
|
|
531
|
+
const auth = loginEndpoint(CLI_AUTH_URL);
|
|
523
532
|
const base = resolveBaseUrl(flags);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (origin !== defaultOrigin && CLI_AUTH_URL.startsWith(defaultOrigin))
|
|
529
|
-
return origin + CLI_AUTH_URL.slice(defaultOrigin.length);
|
|
530
|
-
}
|
|
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;
|
|
531
537
|
}
|
|
532
|
-
|
|
533
|
-
return CLI_AUTH_URL;
|
|
538
|
+
return auth.href;
|
|
534
539
|
}
|
|
535
|
-
/** Browser approval, start to key: opens (or prints) the approval URL and
|
|
536
|
-
* polls until a person decides. Returns the minted credential; every
|
|
537
|
-
* failure exits with the envelope. Shared by `login` and `init`. */
|
|
538
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) {
|
|
539
553
|
const first = AUTH_SCALARS[0];
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const source = headless && !process.stdin.isTTY ? "agent" : "cli";
|
|
545
|
-
let start;
|
|
546
|
-
try {
|
|
547
|
-
const response = await fetch(authUrl + "/start", {
|
|
548
|
-
method: "POST",
|
|
549
|
-
headers: { "Content-Type": "application/json" },
|
|
550
|
-
body: JSON.stringify({ code_challenge: challenge, name, source }),
|
|
551
|
-
signal: AbortSignal.timeout(15_000),
|
|
552
|
-
});
|
|
553
|
-
start = await response.json();
|
|
554
|
-
if (!response.ok || !start.session || !start.verification_url) {
|
|
555
|
-
failWith({ code: "CALL_FAILED", message: "Could not start the browser login: " + (start.error ?? "HTTP " + response.status), nextSteps: ["Pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
catch (e) {
|
|
559
|
-
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>'."] });
|
|
560
|
-
}
|
|
561
|
-
const url = start.verification_url;
|
|
562
|
-
const expiresIn = start.expires_in ?? 600;
|
|
563
|
-
const intervalMs = Math.max(1, start.interval ?? 3) * 1000;
|
|
564
|
-
process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", url) + "\n");
|
|
565
|
-
if (headless) {
|
|
566
|
-
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");
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
openInBrowser(url);
|
|
570
|
-
}
|
|
571
|
-
const deadline = Date.now() + expiresIn * 1000;
|
|
572
|
-
while (Date.now() < deadline) {
|
|
573
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
574
|
-
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 () => {
|
|
575
558
|
try {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
});
|
|
582
|
-
|
|
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");
|
|
583
567
|
}
|
|
584
568
|
catch {
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
if (poll.status === "pending")
|
|
588
|
-
continue;
|
|
589
|
-
if (poll.status === "complete" && poll.api_key) {
|
|
590
|
-
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");
|
|
591
570
|
}
|
|
592
|
-
|
|
593
|
-
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 + "."] });
|
|
594
|
-
if (poll.status === "expired")
|
|
595
|
-
break;
|
|
596
|
-
failWith({ code: "CALL_FAILED", message: "Browser login stopped: " + (poll.status ?? "unknown status"), nextSteps: ["Run '" + BIN + " login' again."] });
|
|
597
|
-
}
|
|
598
|
-
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>'."] });
|
|
599
|
-
}
|
|
600
|
-
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
601
|
-
function storeMinted(stored, minted) {
|
|
602
|
-
const first = AUTH_SCALARS[0];
|
|
603
|
-
stored.scalars = { ...stored.scalars, [first.option]: minted.api_key };
|
|
604
|
-
stored.minted = { via: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}) };
|
|
605
|
-
writeCreds(stored);
|
|
571
|
+
});
|
|
606
572
|
}
|
|
607
|
-
async function browserLogin(
|
|
573
|
+
async function browserLogin(headless, flags) {
|
|
574
|
+
await credentialStore().prepare();
|
|
608
575
|
const minted = await browserApprove(headless, flags);
|
|
609
|
-
storeMinted(
|
|
610
|
-
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);
|
|
611
661
|
await flushExit(0);
|
|
612
662
|
}
|
|
613
663
|
async function cmdLogin(parsed) {
|
|
@@ -618,16 +668,34 @@ async function cmdLogin(parsed) {
|
|
|
618
668
|
...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
|
|
619
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)"] : []),
|
|
620
670
|
" " + BIN + " login --with-token read the credential from stdin (CI)",
|
|
621
|
-
|
|
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"] : []),
|
|
622
674
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
623
|
-
...(CLI_AUTH_URL ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
675
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
624
676
|
"",
|
|
625
|
-
"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.",
|
|
626
685
|
];
|
|
627
686
|
process.stdout.write(lines.join("\n") + "\n");
|
|
628
687
|
await flushExit(0);
|
|
629
688
|
}
|
|
630
|
-
|
|
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);
|
|
631
699
|
const scalarValues = {};
|
|
632
700
|
for (const a of AUTH_SCALARS) {
|
|
633
701
|
const v = parsed.flags.get(a.flag);
|
|
@@ -637,31 +705,38 @@ async function cmdLogin(parsed) {
|
|
|
637
705
|
const username = parsed.flags.get("username");
|
|
638
706
|
const password = parsed.flags.get("password");
|
|
639
707
|
const gotBasic = BASIC !== null && typeof username === "string" && typeof password === "string";
|
|
640
|
-
if (Object.keys(scalarValues).length > 0 || gotBasic) {
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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() });
|
|
647
717
|
await flushExit(0);
|
|
648
718
|
}
|
|
649
719
|
if (parsed.flags.get("with-token") === true) {
|
|
650
720
|
const token = (await readStdin()).trim();
|
|
651
721
|
if (!token)
|
|
652
722
|
fail(2, "--with-token expects the credential on stdin.");
|
|
653
|
-
await storePastedToken(
|
|
723
|
+
await storePastedToken(token, parsed.flags);
|
|
654
724
|
}
|
|
655
725
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") : undefined)
|
|
656
726
|
?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
|
|
657
|
-
if (
|
|
658
|
-
|
|
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);
|
|
659
734
|
}
|
|
660
735
|
// Browser approval: the API mints a key for this CLI once a person
|
|
661
736
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
662
737
|
// polls); only the explicit non-interactive switch turns it off.
|
|
663
738
|
if (CLI_AUTH_URL && AUTH_SCALARS[0] && !explicitNonInteractive(parsed)) {
|
|
664
|
-
await browserLogin(
|
|
739
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
665
740
|
}
|
|
666
741
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
667
742
|
failWith({
|
|
@@ -671,7 +746,7 @@ async function cmdLogin(parsed) {
|
|
|
671
746
|
nextSteps: [
|
|
672
747
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
673
748
|
"Pipe it: echo \"$TOKEN\" | " + BIN + " login --with-token",
|
|
674
|
-
...(
|
|
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)."] : []),
|
|
675
750
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
676
751
|
],
|
|
677
752
|
});
|
|
@@ -682,34 +757,49 @@ async function cmdLogin(parsed) {
|
|
|
682
757
|
const token = (await promptHidden("Paste " + first.flag.replace(/-/g, " ") + " (input hidden): ")).trim();
|
|
683
758
|
if (!token)
|
|
684
759
|
fail(2, "Nothing entered.");
|
|
685
|
-
await storePastedToken(
|
|
760
|
+
await storePastedToken(token, parsed.flags);
|
|
686
761
|
}
|
|
687
|
-
async function cmdLogout() {
|
|
688
|
-
|
|
689
|
-
|
|
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;
|
|
690
770
|
// A key this CLI minted for itself (browser approval) is revoked on the
|
|
691
771
|
// way out, so logging out ends the credential and not just the file. A
|
|
692
772
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
693
773
|
let revoked = null;
|
|
694
774
|
const first = AUTH_SCALARS[0];
|
|
695
775
|
const ownKey = first && stored?.minted?.via === "browser" ? stored.scalars?.[first.option] : undefined;
|
|
696
|
-
if (ownKey
|
|
776
|
+
if (ownKey) {
|
|
697
777
|
try {
|
|
698
|
-
const response = await
|
|
699
|
-
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;
|
|
700
791
|
}
|
|
701
792
|
catch {
|
|
702
793
|
revoked = false;
|
|
703
794
|
}
|
|
704
795
|
}
|
|
705
|
-
rmSync(credsPath(), { force: true });
|
|
706
796
|
out({ ok: true, removed: existed ? credsPath() : null, ...(revoked === null ? {} : { revoked, key_name: stored?.minted?.key_name }) });
|
|
707
797
|
await flushExit(0);
|
|
708
798
|
}
|
|
709
799
|
async function cmdWhoami(parsed) {
|
|
710
800
|
const op = WHOAMI ? OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method) : undefined;
|
|
711
801
|
if (op) {
|
|
712
|
-
const client = await makeClient(parsed.flags);
|
|
802
|
+
const client = await makeClient(parsed.flags, op);
|
|
713
803
|
const target = client[op.resource];
|
|
714
804
|
const result = await target[op.method]();
|
|
715
805
|
if (result.ok) {
|
|
@@ -718,36 +808,14 @@ async function cmdWhoami(parsed) {
|
|
|
718
808
|
}
|
|
719
809
|
failApi(result.error, LAST_CLIENT_HAD_CREDENTIAL);
|
|
720
810
|
}
|
|
721
|
-
const
|
|
722
|
-
let source = "none";
|
|
723
|
-
if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string")) {
|
|
724
|
-
source = "flags";
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
728
|
-
if (envScalar)
|
|
729
|
-
source = "env:" + envScalar.env;
|
|
730
|
-
else if (stored && (stored.scalars || stored.basic || stored.oauth))
|
|
731
|
-
source = "login";
|
|
732
|
-
}
|
|
811
|
+
const source = credentialSource(parsed.flags) ?? "none";
|
|
733
812
|
out({ authenticated: source !== "none", source, credentials: existsSync(credsPath()) ? credsPath() : null });
|
|
734
813
|
await flushExit(source === "none" ? 1 : 0);
|
|
735
814
|
}
|
|
736
815
|
function configFilePath() {
|
|
737
816
|
return join(configDir(), "config.json");
|
|
738
817
|
}
|
|
739
|
-
function readConfig() {
|
|
740
|
-
try {
|
|
741
|
-
return JSON.parse(readFileSync(configFilePath(), "utf8"));
|
|
742
|
-
}
|
|
743
|
-
catch {
|
|
744
|
-
return {};
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
function writeConfig(config) {
|
|
748
|
-
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
749
|
-
writeFileSync(configFilePath(), JSON.stringify(config, null, 2) + "\n");
|
|
750
|
-
}
|
|
818
|
+
function readConfig() { return readProfileConfig(configDir()); }
|
|
751
819
|
const CONFIG_KEYS = ["base-url", "environment", "docs-url"];
|
|
752
820
|
function configGet(config, key) {
|
|
753
821
|
return key === "base-url" ? config.baseUrl : key === "environment" ? config.environment : config.docsUrl;
|
|
@@ -778,6 +846,7 @@ async function cmdConfig(parsed) {
|
|
|
778
846
|
if (sub === undefined || sub === "list") {
|
|
779
847
|
const config = readConfig();
|
|
780
848
|
out({
|
|
849
|
+
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
781
850
|
base_url: config.baseUrl ?? null,
|
|
782
851
|
environment: config.environment ?? null,
|
|
783
852
|
docs_url: config.docsUrl ?? DOCS_URL_DEFAULT,
|
|
@@ -800,13 +869,16 @@ async function cmdConfig(parsed) {
|
|
|
800
869
|
await flushExit(0);
|
|
801
870
|
}
|
|
802
871
|
if (sub === "unset") {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
+
});
|
|
810
882
|
out({ ok: true });
|
|
811
883
|
await flushExit(0);
|
|
812
884
|
}
|
|
@@ -819,10 +891,6 @@ async function cmdConfig(parsed) {
|
|
|
819
891
|
catch {
|
|
820
892
|
fail(2, key + " must be a valid URL");
|
|
821
893
|
}
|
|
822
|
-
if (key === "base-url")
|
|
823
|
-
config.baseUrl = value;
|
|
824
|
-
else
|
|
825
|
-
config.docsUrl = value;
|
|
826
894
|
}
|
|
827
895
|
else {
|
|
828
896
|
if (!(value in ENVIRONMENTS)) {
|
|
@@ -830,9 +898,8 @@ async function cmdConfig(parsed) {
|
|
|
830
898
|
? "environment must be one of: " + Object.keys(ENVIRONMENTS).join(", ")
|
|
831
899
|
: "The spec declares no named environments; use 'config set base-url' instead.");
|
|
832
900
|
}
|
|
833
|
-
config.environment = value;
|
|
834
901
|
}
|
|
835
|
-
|
|
902
|
+
await updateProfileConfig(configDir(), (current) => key === "base-url" ? { ...current, baseUrl: value } : key === "docs-url" ? { ...current, docsUrl: value } : { ...current, environment: value, baseUrl: undefined });
|
|
836
903
|
out({ ok: true, [key.replace(/-/g, "_")]: value });
|
|
837
904
|
await flushExit(0);
|
|
838
905
|
}
|
|
@@ -874,7 +941,12 @@ function mcpEntryFor(url, readOnly = false) {
|
|
|
874
941
|
const server = mcpServerPath();
|
|
875
942
|
if (server.warning)
|
|
876
943
|
warnings.push(server.warning);
|
|
877
|
-
|
|
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 };
|
|
878
950
|
}
|
|
879
951
|
const MCP_CLIENT_FLAGS = {
|
|
880
952
|
claude: "claude-code", cursor: "cursor", "claude-desktop": "claude-desktop", codex: "codex", vscode: "vscode",
|
|
@@ -973,7 +1045,7 @@ function agentContext() {
|
|
|
973
1045
|
apiTitle: API_TITLE,
|
|
974
1046
|
version: VERSION,
|
|
975
1047
|
envPrefix: ENV_PREFIX,
|
|
976
|
-
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"] : [])],
|
|
977
1049
|
docsUrl: docsSiteUrl(),
|
|
978
1050
|
docsIndexUrl: docsIndexUrl(),
|
|
979
1051
|
generatedOperationCount: OPS.length,
|
|
@@ -1050,7 +1122,7 @@ function helpJson() {
|
|
|
1050
1122
|
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1051
1123
|
},
|
|
1052
1124
|
builtins: BUILTIN_COMMANDS,
|
|
1053
|
-
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>")],
|
|
1054
1126
|
auth_env_vars: agentContext().authEnvVars,
|
|
1055
1127
|
};
|
|
1056
1128
|
}
|
|
@@ -1065,32 +1137,48 @@ async function cmdAgentGuide(parsed) {
|
|
|
1065
1137
|
/** Which credential the CLI would send, without sending it. --live calls the identity endpoint too. */
|
|
1066
1138
|
async function cmdAuth(parsed) {
|
|
1067
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
|
+
}
|
|
1068
1156
|
if (parsed.help || sub !== "check") {
|
|
1069
1157
|
process.stdout.write([
|
|
1070
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.",
|
|
1071
1163
|
" --live also call the API's identity endpoint" + (WHOAMI ? "" : " (none in this API; --live is a no-op)"),
|
|
1072
1164
|
"",
|
|
1073
1165
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1074
1166
|
].join("\n") + "\n");
|
|
1075
1167
|
await flushExit(parsed.help ? 0 : 2);
|
|
1076
1168
|
}
|
|
1077
|
-
const
|
|
1078
|
-
let source = "none";
|
|
1079
|
-
if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string"))
|
|
1080
|
-
source = "flags";
|
|
1081
|
-
else {
|
|
1082
|
-
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
1083
|
-
if (envScalar)
|
|
1084
|
-
source = "env:" + envScalar.env;
|
|
1085
|
-
else if (stored && (stored.scalars || stored.basic || stored.oauth))
|
|
1086
|
-
source = "login";
|
|
1087
|
-
}
|
|
1169
|
+
const source = credentialSource(parsed.flags) ?? "none";
|
|
1088
1170
|
const authenticated = source !== "none";
|
|
1171
|
+
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1172
|
+
if (savedIdentity)
|
|
1173
|
+
assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1089
1174
|
const report = {
|
|
1090
1175
|
status: authenticated ? "ok" : "action_required",
|
|
1091
1176
|
authenticated,
|
|
1092
1177
|
source,
|
|
1093
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,
|
|
1094
1182
|
auth_env_vars: agentContext().authEnvVars,
|
|
1095
1183
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1096
1184
|
next_steps: authenticated ? [] : [
|
|
@@ -1101,11 +1189,14 @@ async function cmdAuth(parsed) {
|
|
|
1101
1189
|
if (authenticated && parsed.flags.get("live") === true && WHOAMI) {
|
|
1102
1190
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1103
1191
|
if (op) {
|
|
1104
|
-
const client = await makeClient(parsed.flags);
|
|
1192
|
+
const client = await makeClient(parsed.flags, op);
|
|
1105
1193
|
const target = client[op.resource];
|
|
1106
1194
|
const result = await target[op.method]();
|
|
1107
|
-
if (result.ok)
|
|
1195
|
+
if (result.ok) {
|
|
1196
|
+
if (identityConfiguration() && savedIdentity?.identity)
|
|
1197
|
+
assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1108
1198
|
report.identity = result.data;
|
|
1199
|
+
}
|
|
1109
1200
|
else {
|
|
1110
1201
|
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1111
1202
|
report.status = "action_required";
|
|
@@ -1126,10 +1217,16 @@ async function cmdDoctor(parsed) {
|
|
|
1126
1217
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
1127
1218
|
checks.push({ name: "node", ok: nodeMajor >= 18, detail: process.version, ...(nodeMajor >= 18 ? {} : { fix: "Install Node 18 or newer." }) });
|
|
1128
1219
|
checks.push({ name: "cli", ok: true, detail: BIN + " " + VERSION + " (" + PKG_NAME + ")" });
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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'.") }) });
|
|
1133
1230
|
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
1134
1231
|
if (baseUrl) {
|
|
1135
1232
|
try {
|
|
@@ -1147,7 +1244,7 @@ async function cmdDoctor(parsed) {
|
|
|
1147
1244
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1148
1245
|
if (op) {
|
|
1149
1246
|
try {
|
|
1150
|
-
const client = await makeClient(parsed.flags);
|
|
1247
|
+
const client = await makeClient(parsed.flags, op);
|
|
1151
1248
|
const target = client[op.resource];
|
|
1152
1249
|
const result = await target[op.method]();
|
|
1153
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." });
|
|
@@ -1200,27 +1297,39 @@ async function cmdInit(parsed) {
|
|
|
1200
1297
|
const cwd = process.cwd();
|
|
1201
1298
|
const harness = detectHarness();
|
|
1202
1299
|
// 1. credential
|
|
1203
|
-
const stored = readCreds() ?? {};
|
|
1204
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() ?? {};
|
|
1205
1303
|
const given = (typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") : undefined)
|
|
1206
1304
|
?? (first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) : undefined);
|
|
1207
|
-
if (
|
|
1208
|
-
|
|
1209
|
-
|
|
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);
|
|
1210
1311
|
report.credential = { status: "stored", path: credsPath() };
|
|
1211
1312
|
}
|
|
1212
1313
|
else if (first && process.env[first.env]) {
|
|
1213
1314
|
report.credential = { status: "env", variable: first.env };
|
|
1214
1315
|
}
|
|
1215
|
-
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) {
|
|
1216
1320
|
report.credential = { status: "stored", path: credsPath() };
|
|
1217
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
|
+
}
|
|
1218
1326
|
else if (first && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1219
1327
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1220
1328
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1221
1329
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1330
|
+
await credentialStore().prepare();
|
|
1222
1331
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1223
|
-
storeMinted(
|
|
1332
|
+
await storeMinted(minted, parsed.flags);
|
|
1224
1333
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1225
1334
|
}
|
|
1226
1335
|
else {
|
|
@@ -1359,7 +1468,7 @@ function completionFlagsFor(op) {
|
|
|
1359
1468
|
}
|
|
1360
1469
|
return { flags, values };
|
|
1361
1470
|
}
|
|
1362
|
-
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)];
|
|
1363
1472
|
const BUILTIN_WORDS = {
|
|
1364
1473
|
config: ["list", "get", "set", "unset", "path"],
|
|
1365
1474
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1631,31 +1740,7 @@ async function cmdDocs(parsed) {
|
|
|
1631
1740
|
.filter((match) => match.score > 0)
|
|
1632
1741
|
.sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
|
|
1633
1742
|
.map((match) => match.op);
|
|
1634
|
-
const
|
|
1635
|
-
const proseMatches = [];
|
|
1636
|
-
if (prose !== null) {
|
|
1637
|
-
let heading = "";
|
|
1638
|
-
const terms = searchTerms(term);
|
|
1639
|
-
for (const line of prose.split("\n")) {
|
|
1640
|
-
if (/^#{1,3} /.test(line))
|
|
1641
|
-
heading = line.replace(/^#+ /, "").trim();
|
|
1642
|
-
else {
|
|
1643
|
-
const lowerHeading = heading.toLowerCase();
|
|
1644
|
-
const lowerLine = line.toLowerCase();
|
|
1645
|
-
const matched = terms.filter((word) => lowerHeading.includes(word) || lowerLine.includes(word));
|
|
1646
|
-
if (matched.length > 0) {
|
|
1647
|
-
const allTerms = matched.length === terms.length;
|
|
1648
|
-
proseMatches.push({
|
|
1649
|
-
heading,
|
|
1650
|
-
excerpt: line.trim().slice(0, 160),
|
|
1651
|
-
score: matched.length * 10 + (allTerms ? 50 : 0) + (lowerHeading.includes(term.toLowerCase()) || lowerLine.includes(term.toLowerCase()) ? 25 : 0),
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
proseMatches.sort((a, b) => b.score - a.score || a.heading.localeCompare(b.heading) || a.excerpt.localeCompare(b.excerpt));
|
|
1657
|
-
}
|
|
1658
|
-
const docsStatus = docsSiteUrl() === null ? "not_configured" : prose === null ? "unavailable" : "ok";
|
|
1743
|
+
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1659
1744
|
if (jsonOutput) {
|
|
1660
1745
|
out({
|
|
1661
1746
|
schema_version: "1",
|
|
@@ -1667,7 +1752,7 @@ async function cmdDocs(parsed) {
|
|
|
1667
1752
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1668
1753
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1669
1754
|
})),
|
|
1670
|
-
guides: proseMatches.slice(0, 15).map((
|
|
1755
|
+
guides: proseMatches.slice(0, 15).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1671
1756
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1672
1757
|
guides_status: docsStatus,
|
|
1673
1758
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1684,7 +1769,7 @@ async function cmdDocs(parsed) {
|
|
|
1684
1769
|
if (proseMatches.length > 0) {
|
|
1685
1770
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1686
1771
|
for (const match of proseMatches.slice(0, 15))
|
|
1687
|
-
lines.push(" " +
|
|
1772
|
+
lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1688
1773
|
}
|
|
1689
1774
|
else if (docsStatus === "not_configured") {
|
|
1690
1775
|
lines.push(...(lines.length > 0 ? [""] : []), "(no docs site configured for guide search: '" + BIN + " config set docs-url <url>')");
|
|
@@ -1704,10 +1789,7 @@ async function cmdDocs(parsed) {
|
|
|
1704
1789
|
let target = page;
|
|
1705
1790
|
if (!/^https?:\/\//.test(target)) {
|
|
1706
1791
|
const index = await fetchDocs("llms.txt");
|
|
1707
|
-
|
|
1708
|
-
const hit = linked.find((u) => u.toLowerCase().includes(target.toLowerCase()));
|
|
1709
|
-
if (hit !== undefined)
|
|
1710
|
-
target = hit;
|
|
1792
|
+
target = docsReadTarget(index, docsSiteUrl(), docsIndexUrl(), target);
|
|
1711
1793
|
}
|
|
1712
1794
|
const text = await fetchDocs(target);
|
|
1713
1795
|
if (text === null) {
|
|
@@ -1926,7 +2008,7 @@ function printRoot(stream = process.stdout) {
|
|
|
1926
2008
|
}
|
|
1927
2009
|
const width = termWidth();
|
|
1928
2010
|
const lines = [];
|
|
1929
|
-
lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.
|
|
2011
|
+
lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.9.1");
|
|
1930
2012
|
lines.push("");
|
|
1931
2013
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
1932
2014
|
lines.push("");
|
|
@@ -1951,11 +2033,11 @@ function printRoot(stream = process.stdout) {
|
|
|
1951
2033
|
lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
|
|
1952
2034
|
}
|
|
1953
2035
|
lines.push("");
|
|
1954
|
-
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)" +
|
|
1955
2037
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
1956
2038
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
1957
2039
|
lines.push(...labeled("Credential env vars: ", [
|
|
1958
|
-
...AUTH_SCALARS.map((a) => a.env),
|
|
2040
|
+
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
1959
2041
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
1960
2042
|
].join(", ") || "none", width, 21));
|
|
1961
2043
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
@@ -2000,6 +2082,8 @@ function printResource(resource, stream = process.stdout) {
|
|
|
2000
2082
|
/** Flags the CLI itself adds to an API command, as [flag, description] rows. */
|
|
2001
2083
|
function commandExtras(op) {
|
|
2002
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"]);
|
|
2003
2087
|
if (op.hasBody && op.bodyKind === "binary")
|
|
2004
2088
|
extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
2005
2089
|
else if (op.hasBody)
|
|
@@ -2051,7 +2135,7 @@ function exampleLine(op) {
|
|
|
2051
2135
|
}
|
|
2052
2136
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2053
2137
|
function authNote(op) {
|
|
2054
|
-
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null ||
|
|
2138
|
+
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2055
2139
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2056
2140
|
}
|
|
2057
2141
|
function printOp(op) {
|
|
@@ -2228,36 +2312,88 @@ function coerce(spec, raw, repeated) {
|
|
|
2228
2312
|
}
|
|
2229
2313
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2230
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
|
+
}
|
|
2231
2350
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2232
2351
|
function credentialSource(flags) {
|
|
2352
|
+
if (Object.keys(flagCredentials(flags)).length)
|
|
2353
|
+
return "flags";
|
|
2233
2354
|
if (AUTH_SCALARS.some((a) => typeof flags.get(a.flag) === "string"))
|
|
2234
2355
|
return "flags";
|
|
2235
2356
|
if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string")
|
|
2236
2357
|
return "flags";
|
|
2358
|
+
if (Object.keys(environmentCredentials()).length)
|
|
2359
|
+
return "env:TYPESHIP_CREDENTIALS";
|
|
2237
2360
|
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
2238
2361
|
if (envScalar)
|
|
2239
2362
|
return "env:" + envScalar.env;
|
|
2240
2363
|
if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
|
|
2241
2364
|
return "env:" + BASIC.envUser;
|
|
2242
2365
|
const stored = readCreds();
|
|
2243
|
-
if (stored && (stored.scalars || stored.basic || stored.oauth))
|
|
2366
|
+
if (stored && (stored.scalars || stored.basic || stored.oauth || stored.named))
|
|
2244
2367
|
return "login";
|
|
2245
2368
|
return null;
|
|
2246
2369
|
}
|
|
2247
2370
|
/** Base URL resolution: --base-url > env > config base-url > config environment > spec default. */
|
|
2248
|
-
function resolveBaseUrl(flags) {
|
|
2249
|
-
const config = readConfig();
|
|
2371
|
+
function resolveBaseUrl(flags, config = readConfig()) {
|
|
2250
2372
|
return (typeof flags.get("base-url") === "string" ? flags.get("base-url") : undefined)
|
|
2251
2373
|
?? process.env["TYPESHIP_BASE_URL"]
|
|
2252
2374
|
?? config.baseUrl
|
|
2253
2375
|
?? (config.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
|
|
2254
2376
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2255
2377
|
}
|
|
2256
|
-
async function makeClient(flags) {
|
|
2257
|
-
const
|
|
2258
|
-
const
|
|
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);
|
|
2259
2390
|
if (baseUrl === undefined)
|
|
2260
2391
|
fail(2, "No base URL. Pass --base-url, set TYPESHIP_BASE_URL, or run '" + BIN + " config set base-url <url>'.");
|
|
2392
|
+
if (stored) {
|
|
2393
|
+
assertCredentialDestination(stored, { apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name });
|
|
2394
|
+
if (!forIdentity)
|
|
2395
|
+
assertStoredIdentity(stored, identityConfiguration());
|
|
2396
|
+
}
|
|
2261
2397
|
const options = { baseUrl };
|
|
2262
2398
|
for (const a of AUTH_SCALARS) {
|
|
2263
2399
|
const v = (typeof flags.get(a.flag) === "string" ? flags.get(a.flag) : undefined)
|
|
@@ -2273,10 +2409,28 @@ async function makeClient(flags) {
|
|
|
2273
2409
|
if (username !== undefined && password !== undefined)
|
|
2274
2410
|
options.basicAuth = { username, password };
|
|
2275
2411
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
if (
|
|
2279
|
-
|
|
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);
|
|
2280
2434
|
}
|
|
2281
2435
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2282
2436
|
options.debug = (event) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
@@ -2289,7 +2443,7 @@ async function makeClient(flags) {
|
|
|
2289
2443
|
if (value !== undefined)
|
|
2290
2444
|
options[g.option] = value;
|
|
2291
2445
|
}
|
|
2292
|
-
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;
|
|
2293
2447
|
// Who is calling: the CLI, under which agent harness, and whether an
|
|
2294
2448
|
// agent is driving. "agent" means a harness was detected or the caller
|
|
2295
2449
|
// said so (--mode agent / env); a bare non-TTY run (CI, a pipeline) is
|
|
@@ -2303,6 +2457,11 @@ async function makeClient(flags) {
|
|
|
2303
2457
|
...options.defaultHeaders,
|
|
2304
2458
|
"User-Agent": PKG_NAME + "-cli/" + VERSION + " (typeship" + (harness ? "; harness=" + harness : "") + caller + ")",
|
|
2305
2459
|
};
|
|
2460
|
+
if (forIdentity) {
|
|
2461
|
+
options.fetch = identityFetch(baseUrl);
|
|
2462
|
+
options.maxRetries = 0;
|
|
2463
|
+
options.timeoutMs = 10_000;
|
|
2464
|
+
}
|
|
2306
2465
|
return new TypeshipClient(options);
|
|
2307
2466
|
}
|
|
2308
2467
|
function editDistance(a, b) {
|
|
@@ -2348,6 +2507,14 @@ const BUILTIN_COMMANDS = ["login", "logout", "whoami", "config", "mcp", "docs",
|
|
|
2348
2507
|
async function main() {
|
|
2349
2508
|
const argv = process.argv.slice(2);
|
|
2350
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);
|
|
2351
2518
|
if (parsed.positionals[0] === "help") {
|
|
2352
2519
|
// help --json: the command surface as data (agents read this once).
|
|
2353
2520
|
if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") {
|
|
@@ -2406,10 +2573,10 @@ async function main() {
|
|
|
2406
2573
|
}
|
|
2407
2574
|
if (resourceCmd === "logout") {
|
|
2408
2575
|
if (parsed.help) {
|
|
2409
|
-
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");
|
|
2410
2577
|
await flushExit(0);
|
|
2411
2578
|
}
|
|
2412
|
-
await cmdLogout();
|
|
2579
|
+
await cmdLogout(parsed);
|
|
2413
2580
|
}
|
|
2414
2581
|
if (resourceCmd === "whoami") {
|
|
2415
2582
|
if (parsed.help) {
|
|
@@ -2482,7 +2649,7 @@ async function main() {
|
|
|
2482
2649
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2483
2650
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2484
2651
|
// check can be exact.
|
|
2485
|
-
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)]);
|
|
2486
2653
|
for (const spec of op.params) {
|
|
2487
2654
|
if (spec.kind === "path")
|
|
2488
2655
|
continue;
|
|
@@ -2538,7 +2705,7 @@ async function main() {
|
|
|
2538
2705
|
}
|
|
2539
2706
|
// The spec says this operation needs a credential and none resolved:
|
|
2540
2707
|
// say so now, locally, instead of sending a request to learn it.
|
|
2541
|
-
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) {
|
|
2542
2709
|
failWith({
|
|
2543
2710
|
status: "action_required",
|
|
2544
2711
|
code: "NO_AUTH",
|
|
@@ -2569,7 +2736,7 @@ async function main() {
|
|
|
2569
2736
|
if (answer !== "y" && answer !== "yes")
|
|
2570
2737
|
failWith({ status: "action_required", code: "CONFIRMATION_REQUIRED", message: "Cancelled.", nextSteps: ["Run again with --force to skip the prompt: " + rerun] });
|
|
2571
2738
|
}
|
|
2572
|
-
const client = await makeClient(parsed.flags);
|
|
2739
|
+
const client = await makeClient(parsed.flags, op);
|
|
2573
2740
|
const selectValue = typeof parsed.flags.get("select") === "string" ? parsed.flags.get("select") : undefined;
|
|
2574
2741
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2575
2742
|
const target = client[op.resource];
|