@typeship-ax/cli 0.8.0 → 0.10.0
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 +3509 -800
- package/api.md +444 -494
- 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 +34 -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 +84 -10
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +127 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +55 -26
- package/dist/types.d.ts +602 -123
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -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 +48 -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 +241 -0
- package/src/schemas.ts +55 -26
- package/src/types.ts +640 -123
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,16 @@
|
|
|
6
6
|
// Exit codes: 0 success, 1 API/transport error, 2 usage error.
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
import {
|
|
9
|
+
import { oauthBrowserLogin, type OAuthLoginSession } from "./oauth-login.js";
|
|
10
|
+
import { oauthDeviceLogin, customBrowserApproval, loginEndpoint, type ApprovedCredential } from "./polling-login.js";
|
|
11
|
+
import { oauthStatusRequest } from "./oauth-request.js";
|
|
12
|
+
import { checkConsoleBrowserLogin } from "./console-login-check.js";
|
|
13
|
+
import { type FileCredentialStore, assertCredentialDestination, assertStoredIdentity, credentialIdentityBinding, type CredentialDestination, oauthSessionToken, sessionBinding, type SessionConfiguration, type StoredCredentials as StoredCreds } from "./oauth-session.js";
|
|
14
|
+
import { createCredentialStore } from "./credential-storage.js";
|
|
15
|
+
import { identityPolicyOf, identityFetch, identityResult, verifyApiIdentity, verifyClientIdentity, readApiIdentity, assertApiIdentity, type ApiIdentity, type IdentityConfiguration, type IdentityPolicy, type VerifiedIdentity } from "./api-identity.js";
|
|
16
|
+
import { parseNamedCredentials, readNamedCredentialsFile, resolveNamedCredentials, namedCredentialAvailability, type NamedCredentials, type CredentialSchemes } from "./named-credentials.js";
|
|
17
|
+
import { resolveProfile, listProfiles, selectProfile, removeProfile, readProfileConfig, updateProfileConfig, type ProfileContext } from "./auth-profiles.js";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
10
19
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
20
|
import { homedir, hostname } from "node:os";
|
|
12
21
|
import { basename, dirname, join } from "node:path";
|
|
@@ -19,20 +28,23 @@ import {
|
|
|
19
28
|
type AgentContext, type CommandSummary, type DoctorCheck, type EnvelopeInput, type IssueCode, type McpEntry, type McpWriteResult,
|
|
20
29
|
} from "./cli-agent.js";
|
|
21
30
|
import { relativeDate } from "./dates.js";
|
|
22
|
-
import { fetchDocsText, resolveDocsContentUrl } from "./docs.js";
|
|
31
|
+
import { docsReadCommand, docsReadTarget, fetchDocsText, resolveDocsContentUrl, searchConnectedGuides } from "./docs.js";
|
|
23
32
|
|
|
24
33
|
|
|
25
34
|
const BIN = "typeship";
|
|
26
35
|
const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
|
|
36
|
+
const NAMED_SCHEMES: CredentialSchemes = {"apiKey":{"kind":"bearer","options":["bearerToken"]}};
|
|
27
37
|
const AUTH_SCALARS: { option: string; flag: string; env: string }[] = [{"option":"bearerToken","flag":"token","env":"TYPESHIP_TOKEN"}];
|
|
28
38
|
const BASIC: { envUser: string; envPass: string } | null = null;
|
|
29
39
|
/** Operations omitted from the generated package by its plan cap. */
|
|
30
40
|
const EXCLUDED_OPS = 0;
|
|
31
41
|
/** Generated CLI operations that are intentionally unavailable to MCP. */
|
|
32
42
|
const MCP_EXCLUDED_OPS = 0;
|
|
33
|
-
const VERSION = "0.
|
|
43
|
+
const VERSION = "0.10.0";
|
|
34
44
|
const API_VERSION = "1.0.0";
|
|
35
45
|
const SPEC_FORMAT = "openapi";
|
|
46
|
+
const IDENTITY_POLICY: IdentityPolicy = {};
|
|
47
|
+
let LOGIN_IDENTITY: VerifiedIdentity | undefined;
|
|
36
48
|
const WHOAMI: { resource: string; method: string } | null = {"resource":"account","method":"retrieve"};
|
|
37
49
|
const ENVIRONMENTS: Record<string, string> = {};
|
|
38
50
|
const HAS_MCP = false;
|
|
@@ -44,6 +56,15 @@ const DOCS_INDEX_URL_DEFAULT: string | null = null;
|
|
|
44
56
|
const RELAY: { mintUrl: string; project: string } | null = null;
|
|
45
57
|
const SUPPORT_URL: string | null = null;
|
|
46
58
|
const OAUTH_TOKEN_URL: string | null = null;
|
|
59
|
+
const OAUTH_ISSUER: string | null = null;
|
|
60
|
+
const OAUTH_DISCOVERY_URL: string | undefined = undefined;
|
|
61
|
+
const OAUTH_DISCOVERY_URLS: string[] = [];
|
|
62
|
+
const OAUTH_DEVICE_URL: string | null = null;
|
|
63
|
+
const HAS_OAUTH_LOGIN = OAUTH_TOKEN_URL !== null || OAUTH_DISCOVERY_URLS.length > 0;
|
|
64
|
+
const OAUTH_LOGIN_METHOD: string = "device";
|
|
65
|
+
const OAUTH_REDIRECT_URI: string | undefined = undefined;
|
|
66
|
+
const OAUTH_ORGANIZATION_PARAMETER: "organization" | "organization_id" | undefined = undefined;
|
|
67
|
+
const OAUTH_AUTHORIZATION_URL: string | undefined = undefined;
|
|
47
68
|
const OAUTH_CLIENT_ID: string | null = null;
|
|
48
69
|
const OAUTH_SCOPES: string[] = [];
|
|
49
70
|
const OAUTH_TOKEN_PARAMS: Record<string, string> = {};
|
|
@@ -70,11 +91,12 @@ interface Parsed {
|
|
|
70
91
|
* is known. */
|
|
71
92
|
const CORE_BOOLEAN_FLAGS = new Set(["all", "version", "non-interactive", "debug", "validate", "yes", "force", "json"]);
|
|
72
93
|
const BUILTIN_BOOLEAN_FLAGS: Record<string, string[]> = {
|
|
73
|
-
login: ["with-token", "no-browser"],
|
|
94
|
+
login: ["with-token", "no-browser", "device"],
|
|
95
|
+
logout: ["local"],
|
|
74
96
|
upgrade: ["check"],
|
|
75
97
|
mcp: ["claude", "cursor", "claude-desktop", "codex", "vscode", "windsurf", "gemini", "opencode", "zed", "all", "read-only"],
|
|
76
98
|
docs: ["web", "schema"],
|
|
77
|
-
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md"],
|
|
99
|
+
init: ["all", "yes", "no-skills", "no-mcp", "no-agents-md", "no-browser"],
|
|
78
100
|
auth: ["live"],
|
|
79
101
|
doctor: [],
|
|
80
102
|
};
|
|
@@ -308,35 +330,84 @@ function failApi(error: unknown, hadCredential: boolean): never {
|
|
|
308
330
|
// credentials — written by `login`, cleared by `logout`
|
|
309
331
|
// ---------------------------------------------------------------------------
|
|
310
332
|
|
|
311
|
-
|
|
312
|
-
scalars?: Record<string, string>;
|
|
313
|
-
basic?: { username: string; password: string };
|
|
314
|
-
oauth?: { accessToken: string; refreshToken?: string; expiresAt?: number };
|
|
315
|
-
/** Set when this CLI minted the stored credential for itself (browser
|
|
316
|
-
* approval), so `logout` knows it may revoke it. A pasted key is not. */
|
|
317
|
-
minted?: { via: "browser"; key_name: string; org_id?: string };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function configDir(): string {
|
|
333
|
+
function configRoot(): string {
|
|
321
334
|
const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
322
335
|
return join(base, BIN);
|
|
323
336
|
}
|
|
337
|
+
let PROFILE: ProfileContext;
|
|
338
|
+
function configDir(): string { return PROFILE.directory; }
|
|
324
339
|
|
|
325
340
|
function credsPath(): string {
|
|
326
|
-
return
|
|
341
|
+
return credentialStore().path;
|
|
327
342
|
}
|
|
328
343
|
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
344
|
+
function credentialStore(): FileCredentialStore { return createCredentialStore(configDir(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE"); }
|
|
345
|
+
function readCreds(): StoredCreds | null { return credentialStore().read(); }
|
|
346
|
+
function sessionConfiguration(baseUrl: string, config = readConfig()): SessionConfiguration {
|
|
347
|
+
return {
|
|
348
|
+
apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name,
|
|
349
|
+
identity: identityConfiguration(),
|
|
350
|
+
clientId: process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID,
|
|
351
|
+
issuer: OAUTH_ISSUER, discoveryUrl: OAUTH_DISCOVERY_URL, tokenUrl: OAUTH_TOKEN_URL,
|
|
352
|
+
authorizationUrl: OAUTH_AUTHORIZATION_URL, scopes: OAUTH_SCOPES,
|
|
353
|
+
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
354
|
+
organizationParameter: OAUTH_ORGANIZATION_PARAMETER,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function identityConfiguration(): IdentityConfiguration | undefined {
|
|
359
|
+
return Object.keys(IDENTITY_POLICY).length && WHOAMI ? { operation: WHOAMI.resource + "." + WHOAMI.method, fields: IDENTITY_POLICY } : undefined;
|
|
360
|
+
}
|
|
361
|
+
function expectedLoginIdentity(flags: Map<string, string | boolean>): ApiIdentity | undefined {
|
|
362
|
+
const expected: ApiIdentity = {};
|
|
363
|
+
for (const kind of ["subject", "account", "organization"] as const) {
|
|
364
|
+
const value = flags.get(kind);
|
|
365
|
+
if (value === undefined) continue;
|
|
366
|
+
if (typeof value !== "string" || !value || !IDENTITY_POLICY[kind]) fail(2, "--" + kind + " requires an expected ID and a configured identity field for that ID.");
|
|
367
|
+
expected[kind] = value as string;
|
|
334
368
|
}
|
|
369
|
+
const selection = requestedLoginOrganization(flags);
|
|
370
|
+
if (selection) {
|
|
371
|
+
if (expected.organization !== undefined && expected.organization !== selection.id) fail(2, "--organization must match --login-organization when both are provided.");
|
|
372
|
+
expected.organization = selection.id;
|
|
373
|
+
}
|
|
374
|
+
return Object.keys(expected).length ? expected : undefined;
|
|
375
|
+
}
|
|
376
|
+
function requestedLoginOrganization(flags: Map<string, string | boolean>): { parameter: "organization" | "organization_id"; id: string } | undefined {
|
|
377
|
+
const id = flags.get("login-organization");
|
|
378
|
+
if (id === undefined) return;
|
|
379
|
+
if (!OAUTH_ORGANIZATION_PARAMETER || !IDENTITY_POLICY.organization) fail(2, "--login-organization requires auth.oauth_organization_parameter and identity_organization configured by the API owner.");
|
|
380
|
+
if (typeof id !== "string" || !id || id.length > 512 || /\s|[\u0000-\u001F\u007F]/.test(id)) fail(2, "--login-organization requires a nonempty provider organization ID without whitespace.");
|
|
381
|
+
return { parameter: OAUTH_ORGANIZATION_PARAMETER!, id: id as string };
|
|
335
382
|
}
|
|
383
|
+
function loginIdentityReport(): Record<string, unknown> { return LOGIN_IDENTITY ? { verified_identity: LOGIN_IDENTITY.values, identity_checked_at: LOGIN_IDENTITY.checkedAt } : {}; }
|
|
336
384
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
385
|
+
/** Each login replaces this profile's credentials and retains its API destination. */
|
|
386
|
+
async function saveLoginCredentials(credentials: StoredCreds, flags: Map<string, string | boolean>, destination?: CredentialDestination, onIdentityFailure?: () => Promise<void>): Promise<void> {
|
|
387
|
+
const config = readConfig();
|
|
388
|
+
const apiBaseUrl = destination?.apiBaseUrl ?? resolveBaseUrl(flags, config);
|
|
389
|
+
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
390
|
+
const url = new URL(apiBaseUrl!);
|
|
391
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) fail(2, "The API base URL must be HTTP or HTTPS without credentials, query, or fragment.");
|
|
392
|
+
const bound = destination ? { apiBaseUrl: destination.apiBaseUrl, environment: destination.environment, profile: destination.profile } : { apiBaseUrl: url.href, environment: config.environment, profile: PROFILE.name };
|
|
393
|
+
const next: StoredCreds = { ...credentials, destination: bound };
|
|
394
|
+
const identity = identityConfiguration();
|
|
395
|
+
if (identity && WHOAMI) {
|
|
396
|
+
const op = OPS.find((op) => op.resource === WHOAMI.resource && op.method === WHOAMI.method);
|
|
397
|
+
if (!op) fail(2, "The identity operation is unavailable. Regenerate this product with the configured identity read.");
|
|
398
|
+
const transport = identityFetch(bound.apiBaseUrl);
|
|
399
|
+
const verified = await verifyApiIdentity(async (anonymous) => {
|
|
400
|
+
const client = anonymous ? new TypeshipClient({ baseUrl: bound.apiBaseUrl, fetch: transport, maxRetries: 0, timeoutMs: 10_000 }) : await makeClient(flags, op!, next, true);
|
|
401
|
+
return identityResult(client, op!);
|
|
402
|
+
}, identity.fields, expectedLoginIdentity(flags)).catch(async (error) => {
|
|
403
|
+
await onIdentityFailure?.();
|
|
404
|
+
throw error;
|
|
405
|
+
});
|
|
406
|
+
next.identity = { ...verified, binding: credentialIdentityBinding(next, identity) };
|
|
407
|
+
LOGIN_IDENTITY = verified;
|
|
408
|
+
}
|
|
409
|
+
await credentialStore().update(() => next);
|
|
410
|
+
await updateProfileConfig(configDir(), (current) => ({ ...current, baseUrl: bound.apiBaseUrl }));
|
|
340
411
|
}
|
|
341
412
|
|
|
342
413
|
function readStdin(): Promise<string> {
|
|
@@ -396,118 +467,41 @@ function promptHidden(promptText: string): Promise<string> {
|
|
|
396
467
|
});
|
|
397
468
|
}
|
|
398
469
|
|
|
399
|
-
async function
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
} catch { /* try the next well-known path */ }
|
|
428
|
-
}
|
|
429
|
-
if (!deviceEndpoint) {
|
|
430
|
-
fail(1, "The authorization server does not advertise a device flow. Run '" + BIN + " login' with a pasted credential instead.");
|
|
431
|
-
}
|
|
432
|
-
const start = await oauthForm(deviceEndpoint!, {
|
|
433
|
-
client_id: clientId,
|
|
434
|
-
...(OAUTH_SCOPES.length > 0 ? { scope: OAUTH_SCOPES.join(" ") } : {}),
|
|
435
|
-
...OAUTH_TOKEN_PARAMS,
|
|
436
|
-
});
|
|
437
|
-
const startBody = start.body as { device_code?: string; user_code?: string; verification_uri?: string; verification_uri_complete?: string; interval?: number; expires_in?: number } | null;
|
|
438
|
-
if (start.status !== 200 || !startBody?.device_code) {
|
|
439
|
-
fail(1, "Device authorization failed (HTTP " + start.status + ").", start.body);
|
|
440
|
-
}
|
|
441
|
-
const uri = startBody!.verification_uri_complete ?? startBody!.verification_uri;
|
|
442
|
-
process.stderr.write("Open " + paintErr("cyan", String(uri)) + " and enter code: " + paintErr("bold", String(startBody!.user_code)) + "\n");
|
|
443
|
-
// Under an agent the same facts also go out as one JSON line (stderr,
|
|
444
|
-
// so stdout stays the single result document), for the agent to hand
|
|
445
|
-
// the URL and code to the user while this polls.
|
|
446
|
-
if (agent) process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: uri, user_code: startBody!.user_code, expires_in: startBody!.expires_in ?? 900 }) + "\n");
|
|
447
|
-
let intervalMs = (startBody!.interval ?? 5) * 1000;
|
|
448
|
-
const deadline = Date.now() + (startBody!.expires_in ?? 900) * 1000;
|
|
449
|
-
while (Date.now() < deadline) {
|
|
450
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
451
|
-
const poll = await oauthForm(tokenEndpoint, {
|
|
452
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
453
|
-
device_code: startBody!.device_code!,
|
|
454
|
-
client_id: clientId,
|
|
455
|
-
});
|
|
456
|
-
const tokenBody = poll.body as { access_token?: string; refresh_token?: string; expires_in?: number; error?: string } | null;
|
|
457
|
-
if (poll.status === 200 && tokenBody?.access_token) {
|
|
458
|
-
writeCreds({
|
|
459
|
-
...(readCreds() ?? {}),
|
|
460
|
-
oauth: {
|
|
461
|
-
accessToken: tokenBody.access_token,
|
|
462
|
-
refreshToken: tokenBody.refresh_token,
|
|
463
|
-
expiresAt: tokenBody.expires_in ? Date.now() + tokenBody.expires_in * 1000 : undefined,
|
|
464
|
-
},
|
|
465
|
-
});
|
|
466
|
-
process.stderr.write(paintErr("green", "Logged in.") + "\n");
|
|
467
|
-
out({ ok: true, method: "device", credentials: credsPath() });
|
|
468
|
-
await flushExit(0);
|
|
469
|
-
}
|
|
470
|
-
const errorCode = tokenBody?.error;
|
|
471
|
-
if (errorCode === "authorization_pending") continue;
|
|
472
|
-
if (errorCode === "slow_down") { intervalMs += 5000; continue; }
|
|
473
|
-
fail(1, "Device login failed: " + (errorCode ?? "HTTP " + poll.status), poll.body);
|
|
474
|
-
}
|
|
475
|
-
fail(1, "Device login timed out before the code was entered.");
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** Stored OAuth access token, refreshed through the token URL when expired.
|
|
479
|
-
* A failed refresh returns the stale token; the API's 401 explains better
|
|
480
|
-
* than a local guess. */
|
|
481
|
-
async function refreshedOauthToken(stored: StoredCreds): Promise<string | undefined> {
|
|
482
|
-
const oauth = stored.oauth;
|
|
483
|
-
if (!oauth) return undefined;
|
|
484
|
-
const expired = oauth.expiresAt !== undefined && Date.now() > oauth.expiresAt - 60_000;
|
|
485
|
-
if (!expired || !oauth.refreshToken || !OAUTH_TOKEN_URL) 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 as { access_token?: string; refresh_token?: string; expires_in?: number } | null;
|
|
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;
|
|
470
|
+
async function withLoginCancellation<T>(work: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
471
|
+
const controller = new AbortController();
|
|
472
|
+
const cancel = () => controller.abort();
|
|
473
|
+
process.once("SIGINT", cancel); process.once("SIGTERM", cancel);
|
|
474
|
+
try { return await work(controller.signal); }
|
|
475
|
+
finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function deviceLogin(clientId: string, parsed: Parsed): Promise<void> {
|
|
479
|
+
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
480
|
+
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
481
|
+
const loginConfiguration = sessionConfiguration(apiBaseUrl!);
|
|
482
|
+
await credentialStore().prepare();
|
|
483
|
+
const session = await withLoginCancellation((signal) => oauthDeviceLogin({
|
|
484
|
+
clientId, issuer: OAUTH_ISSUER, discoveryUrls: OAUTH_DISCOVERY_URLS,
|
|
485
|
+
deviceUrl: OAUTH_DEVICE_URL, tokenUrl: OAUTH_TOKEN_URL, scopes: OAUTH_SCOPES,
|
|
486
|
+
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
487
|
+
}, { signal, authorize({ verificationUri, userCode, expiresIn }) {
|
|
488
|
+
process.stderr.write("Open " + paintErr("cyan", verificationUri) + " and enter code: " + paintErr("bold", userCode) + "\n");
|
|
489
|
+
if (isAgentMode(parsed)) process.stderr.write(JSON.stringify({ event: "device_code", verification_uri: verificationUri, user_code: userCode, expires_in: expiresIn }) + "\n");
|
|
490
|
+
} }));
|
|
491
|
+
await saveLoginCredentials({ oauth: { ...session,
|
|
492
|
+
sessionId: randomBytes(16).toString("hex"), binding: sessionBinding(loginConfiguration),
|
|
493
|
+
apiBaseUrl, configuredClientId: loginConfiguration.clientId ?? null,
|
|
494
|
+
} }, parsed.flags, loginConfiguration);
|
|
495
|
+
process.stderr.write(paintErr("green", "Logged in.") + "\n");
|
|
496
|
+
out({ ok: true, method: "device", credentials: credsPath(), ...loginIdentityReport() });
|
|
497
|
+
await flushExit(0);
|
|
503
498
|
}
|
|
504
499
|
|
|
505
|
-
async function storePastedToken(
|
|
500
|
+
async function storePastedToken(token: string, flags: Map<string, string | boolean>): Promise<void> {
|
|
506
501
|
const first = AUTH_SCALARS[0];
|
|
507
502
|
if (!first) 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() });
|
|
503
|
+
await saveLoginCredentials({ scalars: { [first!.option]: token } }, flags);
|
|
504
|
+
out({ ok: true, method: "paste", stored_as: first!.flag, credentials: credsPath(), ...loginIdentityReport() });
|
|
511
505
|
await flushExit(0);
|
|
512
506
|
}
|
|
513
507
|
|
|
@@ -518,92 +512,126 @@ async function storePastedToken(stored: StoredCreds, token: string): Promise<voi
|
|
|
518
512
|
* until the API hands back a key minted for this CLI, store it. The key
|
|
519
513
|
* never crosses the chat: the agent relays a URL, the person clicks once.
|
|
520
514
|
*/
|
|
521
|
-
/**
|
|
522
|
-
*
|
|
515
|
+
/** A same-origin API override uses that deployment's approval route. A
|
|
516
|
+
* separately hosted identity service retains its explicitly configured URL. */
|
|
523
517
|
function cliAuthUrl(flags: Map<string, string | boolean>): string {
|
|
518
|
+
const auth = loginEndpoint(CLI_AUTH_URL!);
|
|
524
519
|
const base = resolveBaseUrl(flags);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
520
|
+
if (DEFAULT_BASE_URL && base && auth.origin === new URL(DEFAULT_BASE_URL).origin) {
|
|
521
|
+
const selected = loginEndpoint(base);
|
|
522
|
+
auth.protocol = selected.protocol; auth.host = selected.host;
|
|
523
|
+
}
|
|
524
|
+
return auth.href;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function browserApprove(headless: boolean, flags: Map<string, string | boolean>): Promise<ApprovedCredential> {
|
|
528
|
+
return withLoginCancellation((signal) => customBrowserApproval({
|
|
529
|
+
authUrl: cliAuthUrl(flags), name: BIN + " CLI on " + hostname(), source: headless && !process.stdin.isTTY ? "agent" : "cli",
|
|
530
|
+
}, { signal, authorize({ verificationUrl, expiresIn }) {
|
|
531
|
+
process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", verificationUrl) + "\n");
|
|
532
|
+
if (headless) 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");
|
|
533
|
+
else openInBrowser(verificationUrl);
|
|
534
|
+
} }));
|
|
533
535
|
}
|
|
534
536
|
|
|
535
|
-
/**
|
|
536
|
-
|
|
537
|
-
* failure exits with the envelope. Shared by `login` and `init`. */
|
|
538
|
-
async function browserApprove(headless: boolean, flags: Map<string, string | boolean>): Promise<{ api_key: string; key_name: string; org_id?: string }> {
|
|
537
|
+
/** Store what the browser approval minted, marked as this CLI's own. */
|
|
538
|
+
async function storeMinted(minted: ApprovedCredential, flags: Map<string, string | boolean>): Promise<void> {
|
|
539
539
|
const first = AUTH_SCALARS[0]!;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const source = headless && !process.stdin.isTTY ? "agent" : "cli";
|
|
545
|
-
let start: { session?: string; verification_url?: string; expires_in?: number; interval?: number; error?: string };
|
|
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() as typeof start;
|
|
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
|
-
} catch (e) {
|
|
558
|
-
failWith({ code: "NETWORK_ERROR", message: "Could not reach " + authUrl + "/start: " + (e as Error).message, nextSteps: ["Check the network, or pass the credential directly: '" + BIN + " login --" + first.flag + " <value>'."] });
|
|
559
|
-
}
|
|
560
|
-
const url = start!.verification_url!;
|
|
561
|
-
const expiresIn = start!.expires_in ?? 600;
|
|
562
|
-
const intervalMs = Math.max(1, start!.interval ?? 3) * 1000;
|
|
563
|
-
process.stderr.write("Approve this CLI in your browser: " + paintErr("cyan", url) + "\n");
|
|
564
|
-
if (headless) {
|
|
565
|
-
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");
|
|
566
|
-
} else {
|
|
567
|
-
openInBrowser(url);
|
|
568
|
-
}
|
|
569
|
-
const deadline = Date.now() + expiresIn * 1000;
|
|
570
|
-
while (Date.now() < deadline) {
|
|
571
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
572
|
-
let poll: { status?: string; api_key?: string; key_name?: string; org_id?: string };
|
|
540
|
+
await saveLoginCredentials({
|
|
541
|
+
scalars: { [first.option]: minted.api_key },
|
|
542
|
+
minted: { via: "browser", key_name: minted.key_name, revocationUrl: minted.revocationUrl, ...(minted.org_id ? { org_id: minted.org_id } : {}) },
|
|
543
|
+
}, flags, undefined, async () => {
|
|
573
544
|
try {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
});
|
|
580
|
-
|
|
545
|
+
const saved = credentialStore().read();
|
|
546
|
+
if ([...Object.values(saved?.scalars ?? {}), ...Object.values(saved?.named ?? {}), saved?.oauth?.accessToken, saved?.oauth?.refreshToken].includes(minted.api_key)) {
|
|
547
|
+
process.stderr.write("The rejected approval returned an existing saved credential; it was not revoked.\n");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const response = await oauthStatusRequest(loginEndpoint(minted.revocationUrl), { method: "POST", headers: { Authorization: "Bearer " + minted.api_key } }, 15_000);
|
|
551
|
+
if (response.status < 200 || response.status >= 300) throw new Error("Revocation failed");
|
|
581
552
|
} catch {
|
|
582
|
-
|
|
553
|
+
process.stderr.write("The rejected approval's new credential could not be revoked. Revoke it in your API account.\n");
|
|
583
554
|
}
|
|
584
|
-
|
|
585
|
-
if (poll.status === "complete" && poll.api_key) {
|
|
586
|
-
return { api_key: poll.api_key, key_name: poll.key_name ?? name, ...(poll.org_id ? { org_id: poll.org_id } : {}) };
|
|
587
|
-
}
|
|
588
|
-
if (poll.status === "denied") 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") break;
|
|
590
|
-
failWith({ code: "CALL_FAILED", message: "Browser login stopped: " + (poll.status ?? "unknown status"), nextSteps: ["Run '" + BIN + " login' again."] });
|
|
591
|
-
}
|
|
592
|
-
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>'."] });
|
|
555
|
+
});
|
|
593
556
|
}
|
|
594
557
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
558
|
+
async function browserLogin(headless: boolean, flags: Map<string, string | boolean>): Promise<void> {
|
|
559
|
+
await credentialStore().prepare();
|
|
560
|
+
const minted = await browserApprove(headless, flags);
|
|
561
|
+
await storeMinted(minted, flags);
|
|
562
|
+
out({ ok: true, method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), credentials: credsPath(), ...loginIdentityReport() });
|
|
563
|
+
await flushExit(0);
|
|
601
564
|
}
|
|
602
565
|
|
|
603
|
-
async function
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
566
|
+
async function acquireOAuthBrowserSession(parsed: Parsed, clientId: string): Promise<void> {
|
|
567
|
+
if (!OAUTH_ISSUER) fail(2, "Browser OAuth requires the exact auth.oauth_issuer configured by the API owner.");
|
|
568
|
+
const apiBaseUrl = resolveBaseUrl(parsed.flags);
|
|
569
|
+
if (!apiBaseUrl) fail(2, "Set the API base URL before logging in.");
|
|
570
|
+
const loginConfiguration = sessionConfiguration(apiBaseUrl!);
|
|
571
|
+
await credentialStore().prepare();
|
|
572
|
+
const session = await startOAuthBrowserSession(parsed, clientId);
|
|
573
|
+
await saveLoginCredentials({ oauth: { ...session, apiBaseUrl,
|
|
574
|
+
sessionId: randomBytes(16).toString("hex"), binding: sessionBinding(loginConfiguration),
|
|
575
|
+
configuredClientId: loginConfiguration.clientId ?? null,
|
|
576
|
+
} }, parsed.flags, loginConfiguration);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Normal login and Console verification use the same native exchange. */
|
|
580
|
+
async function startOAuthBrowserSession(parsed: Parsed, clientId: string, timeoutMs?: number): Promise<OAuthLoginSession> {
|
|
581
|
+
const controller = new AbortController();
|
|
582
|
+
const cancel = () => controller.abort();
|
|
583
|
+
process.once("SIGINT", cancel);
|
|
584
|
+
process.once("SIGTERM", cancel);
|
|
585
|
+
try {
|
|
586
|
+
return await oauthBrowserLogin({
|
|
587
|
+
issuer: OAUTH_ISSUER!, clientId, discoveryUrl: OAUTH_DISCOVERY_URL,
|
|
588
|
+
authorizationUrl: OAUTH_AUTHORIZATION_URL, tokenUrl: OAUTH_TOKEN_URL ?? undefined,
|
|
589
|
+
redirectUri: OAUTH_REDIRECT_URI, scopes: OAUTH_SCOPES,
|
|
590
|
+
audience: OAUTH_TOKEN_PARAMS.audience, resource: OAUTH_TOKEN_PARAMS.resource,
|
|
591
|
+
organization: requestedLoginOrganization(parsed.flags),
|
|
592
|
+
}, { signal: controller.signal, timeoutMs, authorize(url) {
|
|
593
|
+
process.stderr.write("Sign in to your existing account: " + url + "\n");
|
|
594
|
+
if (isAgentMode(parsed) || parsed.flags.get("no-browser") === true) 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");
|
|
595
|
+
else openInBrowser(url);
|
|
596
|
+
} });
|
|
597
|
+
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function cmdConsoleLoginCheck(parsed: Parsed): Promise<void> {
|
|
601
|
+
const file = parsed.flags.get("console-check");
|
|
602
|
+
if (typeof file !== "string" || !file || parsed.flags.get("device") === true || parsed.flags.get("with-token") === true || explicitNonInteractive(parsed)) fail(2, "--console-check requires a downloaded JSON file and browser login. Use --no-browser to print the sign-in link.");
|
|
603
|
+
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
604
|
+
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") as string : undefined) ?? process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID;
|
|
605
|
+
const op = WHOAMI && OPS.find((value) => value.resource === WHOAMI.resource && value.method === WHOAMI.method);
|
|
606
|
+
if (!baseUrl || !OAUTH_ISSUER || !clientId || OAUTH_LOGIN_METHOD !== "browser" || !op || op.auth !== "required" || !op.security?.length) fail(2, "Configure browser OAuth and a required-authentication identity read, then regenerate this CLI.");
|
|
607
|
+
const envOptions: Record<string, unknown> = {}, flagOptions: Record<string, unknown> = {};
|
|
608
|
+
for (const scalar of AUTH_SCALARS) {
|
|
609
|
+
if (process.env[scalar.env] !== undefined) envOptions[scalar.option] = process.env[scalar.env];
|
|
610
|
+
if (typeof parsed.flags.get(scalar.flag) === "string") flagOptions[scalar.option] = parsed.flags.get(scalar.flag);
|
|
611
|
+
}
|
|
612
|
+
if (BASIC && process.env[BASIC.envUser] && process.env[BASIC.envPass]) envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
613
|
+
if (BASIC && typeof parsed.flags.get("username") === "string" && typeof parsed.flags.get("password") === "string") flagOptions.basicAuth = { username: parsed.flags.get("username"), password: parsed.flags.get("password") };
|
|
614
|
+
const environment = (typeof parsed.flags.get("environment") === "string" ? parsed.flags.get("environment") as string : Object.entries(ENVIRONMENTS).find(([, url]) => new URL(url).href === new URL(baseUrl!).href)?.[0]) ?? null;
|
|
615
|
+
const result = await checkConsoleBrowserLogin({
|
|
616
|
+
file: file as string, configuration: {
|
|
617
|
+
baseUrl: baseUrl!, environment, operation: op!.resource + "." + op!.method, requirements: op!.security!,
|
|
618
|
+
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 },
|
|
619
|
+
issuer: OAUTH_ISSUER!, clientId: clientId!, discoveryUrl: OAUTH_DISCOVERY_URL ?? null,
|
|
620
|
+
authorizationUrl: OAUTH_AUTHORIZATION_URL ?? null, tokenUrl: OAUTH_TOKEN_URL,
|
|
621
|
+
redirectUri: OAUTH_REDIRECT_URI ?? "http://127.0.0.1/callback", scopes: OAUTH_SCOPES,
|
|
622
|
+
audience: OAUTH_TOKEN_PARAMS.audience ?? null, resource: OAUTH_TOKEN_PARAMS.resource ?? null,
|
|
623
|
+
}, schemes: NAMED_SCHEMES,
|
|
624
|
+
credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ options: envOptions, named: environmentCredentials() }, { options: flagOptions, named: flagCredentials(parsed.flags) }]),
|
|
625
|
+
login: (timeoutMs) => startOAuthBrowserSession(parsed, clientId!, timeoutMs),
|
|
626
|
+
async verify(credentials, expectations) {
|
|
627
|
+
const kind = (value: "user" | "account" | "organization") => value === "user" ? "subject" : value;
|
|
628
|
+
const policy = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), entry.pointer])) as IdentityPolicy;
|
|
629
|
+
const expected = Object.fromEntries(expectations.map((entry) => [kind(entry.kind), String(entry.expected)])) as ApiIdentity;
|
|
630
|
+
await verifyClientIdentity((options) => new TypeshipClient(options as unknown as ClientOptions), { baseUrl: baseUrl!, credentials }, op!, policy, expected);
|
|
631
|
+
},
|
|
632
|
+
progress: (message) => process.stderr.write(message + "\n"),
|
|
633
|
+
});
|
|
634
|
+
out(result);
|
|
607
635
|
await flushExit(0);
|
|
608
636
|
}
|
|
609
637
|
|
|
@@ -615,17 +643,30 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
615
643
|
...AUTH_SCALARS.map((a) => " " + BIN + " login --" + a.flag + " <value>"),
|
|
616
644
|
...(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)"] : []),
|
|
617
645
|
" " + BIN + " login --with-token read the credential from stdin (CI)",
|
|
618
|
-
|
|
646
|
+
" " + BIN + " login --console-check <file> test browser login for your Typeship Console without replacing saved logins",
|
|
647
|
+
...(HAS_OAUTH_LOGIN ? [" " + BIN + " login --client-id <id> OAuth " + OAUTH_LOGIN_METHOD + " login" + (OAUTH_CLIENT_ID ? " (a default id is built in)" : "")] : []),
|
|
648
|
+
...(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"] : []),
|
|
619
649
|
...(BASIC ? [" " + BIN + " login --username <u> --password <p>"] : []),
|
|
620
|
-
...(CLI_AUTH_URL ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
650
|
+
...(CLI_AUTH_URL || HAS_OAUTH_LOGIN ? [] : [" " + BIN + " login interactive prompt (TTY only)"]),
|
|
621
651
|
"",
|
|
622
|
-
"Precedence
|
|
652
|
+
"Precedence per scheme: flags > env vars > stored credentials. Named inputs beat convenience flags within the same source.",
|
|
653
|
+
" " + BIN + " login --credentials @<JSON-file> store named credentials (use - for stdin)",
|
|
654
|
+
"Named schemes: " + Object.entries(NAMED_SCHEMES).map(([name, scheme]) => name + " (" + scheme.kind + ")").join(", "),
|
|
655
|
+
"JSON values are tokens/API keys, or {username, password} for Basic auth. Runtime env: TYPESHIP_CREDENTIALS.",
|
|
656
|
+
"--profile <name> selects an isolated login. Use auth profiles to list profiles and auth use <name> to select a default.",
|
|
657
|
+
...(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."] : []),
|
|
658
|
+
...(OAUTH_ORGANIZATION_PARAMETER ? ["--login-organization <id> requests that provider organization during browser login and requires the API to confirm the same ID."] : []),
|
|
659
|
+
"Saved credentials use OS protection by default. TYPESHIP_CREDENTIAL_STORE=file explicitly opts into plaintext storage.",
|
|
623
660
|
];
|
|
624
661
|
process.stdout.write(lines.join("\n") + "\n");
|
|
625
662
|
await flushExit(0);
|
|
626
663
|
}
|
|
627
|
-
|
|
628
|
-
|
|
664
|
+
if (parsed.flags.has("login-organization")) {
|
|
665
|
+
expectedLoginIdentity(parsed.flags);
|
|
666
|
+
if (!HAS_OAUTH_LOGIN || OAUTH_LOGIN_METHOD !== "browser" || parsed.flags.has("device") || parsed.flags.has("console-check") || parsed.flags.has("with-token") || explicitNonInteractive(parsed)) fail(2, "--login-organization is available only for interactive OAuth browser login, including --no-browser.");
|
|
667
|
+
}
|
|
668
|
+
if (parsed.flags.has("console-check")) { await cmdConsoleLoginCheck(parsed); return; }
|
|
669
|
+
const named = flagCredentials(parsed.flags);
|
|
629
670
|
const scalarValues: Record<string, string> = {};
|
|
630
671
|
for (const a of AUTH_SCALARS) {
|
|
631
672
|
const v = parsed.flags.get(a.flag);
|
|
@@ -634,31 +675,39 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
634
675
|
const username = parsed.flags.get("username");
|
|
635
676
|
const password = parsed.flags.get("password");
|
|
636
677
|
const gotBasic = BASIC !== null && typeof username === "string" && typeof password === "string";
|
|
637
|
-
if (Object.keys(scalarValues).length > 0 || gotBasic) {
|
|
638
|
-
if (
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
678
|
+
if (Object.keys(scalarValues).length > 0 || gotBasic || Object.keys(named).length > 0) {
|
|
679
|
+
if (parsed.flags.has("login-organization")) fail(2, "--login-organization cannot be combined with pasted credentials.");
|
|
680
|
+
await saveLoginCredentials({
|
|
681
|
+
...(Object.keys(named).length ? { named } : {}),
|
|
682
|
+
...(Object.keys(scalarValues).length > 0 ? { scalars: scalarValues } : {}),
|
|
683
|
+
...(gotBasic ? { basic: { username: username as string, password: password as string } } : {}),
|
|
684
|
+
}, parsed.flags);
|
|
685
|
+
out({ ok: true, method: "flags", credentials: credsPath(), ...loginIdentityReport() });
|
|
642
686
|
await flushExit(0);
|
|
643
687
|
}
|
|
644
688
|
|
|
645
689
|
if (parsed.flags.get("with-token") === true) {
|
|
646
690
|
const token = (await readStdin()).trim();
|
|
647
691
|
if (!token) fail(2, "--with-token expects the credential on stdin.");
|
|
648
|
-
await storePastedToken(
|
|
692
|
+
await storePastedToken(token, parsed.flags);
|
|
649
693
|
}
|
|
650
694
|
|
|
651
695
|
const clientId = (typeof parsed.flags.get("client-id") === "string" ? parsed.flags.get("client-id") as string : undefined)
|
|
652
696
|
?? process.env["TYPESHIP_CLIENT_ID"] ?? OAUTH_CLIENT_ID ?? undefined;
|
|
653
|
-
if (
|
|
654
|
-
|
|
697
|
+
if (HAS_OAUTH_LOGIN && clientId !== undefined && !explicitNonInteractive(parsed)) {
|
|
698
|
+
if (OAUTH_LOGIN_METHOD === "browser" && parsed.flags.get("device") !== true) {
|
|
699
|
+
await acquireOAuthBrowserSession(parsed, clientId);
|
|
700
|
+
out({ ok: true, method: "oauth_browser", credentials: credsPath(), ...loginIdentityReport() });
|
|
701
|
+
await flushExit(0);
|
|
702
|
+
}
|
|
703
|
+
await deviceLogin(clientId, parsed);
|
|
655
704
|
}
|
|
656
705
|
|
|
657
706
|
// Browser approval: the API mints a key for this CLI once a person
|
|
658
707
|
// approves in the browser. Works under an agent too (it prints the URL and
|
|
659
708
|
// polls); only the explicit non-interactive switch turns it off.
|
|
660
709
|
if (CLI_AUTH_URL && AUTH_SCALARS[0] && !explicitNonInteractive(parsed)) {
|
|
661
|
-
await browserLogin(
|
|
710
|
+
await browserLogin(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
662
711
|
}
|
|
663
712
|
|
|
664
713
|
if (nonInteractive(parsed) || !process.stdin.isTTY) {
|
|
@@ -669,7 +718,7 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
669
718
|
nextSteps: [
|
|
670
719
|
...AUTH_SCALARS.map((a) => "Pass the credential: '" + BIN + " login --" + a.flag + " <value>', or set " + a.env + " in the environment."),
|
|
671
720
|
"Pipe it: echo \"$TOKEN\" | " + BIN + " login --with-token",
|
|
672
|
-
...(
|
|
721
|
+
...(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)."] : []),
|
|
673
722
|
...(CLI_AUTH_URL ? ["Browser approval: '" + BIN + " login --no-browser' prints a link for the user to approve and waits."] : []),
|
|
674
723
|
],
|
|
675
724
|
});
|
|
@@ -678,27 +727,39 @@ async function cmdLogin(parsed: Parsed): Promise<void> {
|
|
|
678
727
|
if (!first) fail(2, "This API declares no credential the CLI can prompt for. See '" + BIN + " login --help'.");
|
|
679
728
|
const token = (await promptHidden("Paste " + first.flag.replace(/-/g, " ") + " (input hidden): ")).trim();
|
|
680
729
|
if (!token) fail(2, "Nothing entered.");
|
|
681
|
-
await storePastedToken(
|
|
730
|
+
await storePastedToken(token, parsed.flags);
|
|
682
731
|
}
|
|
683
732
|
|
|
684
|
-
async function cmdLogout(): Promise<void> {
|
|
685
|
-
|
|
686
|
-
|
|
733
|
+
async function cmdLogout(parsed: Parsed): Promise<void> {
|
|
734
|
+
if (parsed.flags.get("local") === true) {
|
|
735
|
+
const removed = await credentialStore().clear();
|
|
736
|
+
out({ ok: true, removed: removed ? credsPath() : null, revoked: false, revocation_skipped: true });
|
|
737
|
+
await flushExit(0);
|
|
738
|
+
}
|
|
739
|
+
const stored = await credentialStore().take();
|
|
740
|
+
const existed = stored !== null;
|
|
687
741
|
// A key this CLI minted for itself (browser approval) is revoked on the
|
|
688
742
|
// way out, so logging out ends the credential and not just the file. A
|
|
689
743
|
// pasted or CI key is someone else's to revoke, and is left alone.
|
|
690
744
|
let revoked: boolean | null = null;
|
|
691
745
|
const first = AUTH_SCALARS[0];
|
|
692
746
|
const ownKey = first && stored?.minted?.via === "browser" ? stored.scalars?.[first.option] : undefined;
|
|
693
|
-
if (ownKey
|
|
747
|
+
if (ownKey) {
|
|
694
748
|
try {
|
|
695
|
-
const response = await
|
|
696
|
-
revoked = response.
|
|
749
|
+
const response = await oauthStatusRequest(loginEndpoint(stored!.minted!.revocationUrl!), { method: "POST", headers: { Authorization: "Bearer " + ownKey } }, 15_000);
|
|
750
|
+
revoked = response.status >= 200 && response.status < 300;
|
|
697
751
|
} catch {
|
|
698
752
|
revoked = false;
|
|
699
753
|
}
|
|
700
754
|
}
|
|
701
|
-
|
|
755
|
+
if (stored?.oauth?.revocationUrl) {
|
|
756
|
+
try {
|
|
757
|
+
const oauth = stored.oauth;
|
|
758
|
+
const token = oauth.refreshToken ?? oauth.accessToken;
|
|
759
|
+
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 } : {}) }) });
|
|
760
|
+
revoked = result.status >= 200 && result.status < 300;
|
|
761
|
+
} catch { revoked = false; }
|
|
762
|
+
}
|
|
702
763
|
out({ ok: true, removed: existed ? credsPath() : null, ...(revoked === null ? {} : { revoked, key_name: stored?.minted?.key_name }) });
|
|
703
764
|
await flushExit(0);
|
|
704
765
|
}
|
|
@@ -706,21 +767,13 @@ async function cmdLogout(): Promise<void> {
|
|
|
706
767
|
async function cmdWhoami(parsed: Parsed): Promise<void> {
|
|
707
768
|
const op = WHOAMI ? OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method) : undefined;
|
|
708
769
|
if (op) {
|
|
709
|
-
const client = await makeClient(parsed.flags);
|
|
770
|
+
const client = await makeClient(parsed.flags, op);
|
|
710
771
|
const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; data?: unknown; error?: unknown }>>>)[op.resource]!;
|
|
711
772
|
const result = await target[op.method]!();
|
|
712
773
|
if (result.ok) { out(result.data ?? { ok: true }); await flushExit(0); }
|
|
713
774
|
failApi(result.error, LAST_CLIENT_HAD_CREDENTIAL);
|
|
714
775
|
}
|
|
715
|
-
const
|
|
716
|
-
let source = "none";
|
|
717
|
-
if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string")) {
|
|
718
|
-
source = "flags";
|
|
719
|
-
} else {
|
|
720
|
-
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
721
|
-
if (envScalar) source = "env:" + envScalar.env;
|
|
722
|
-
else if (stored && (stored.scalars || stored.basic || stored.oauth)) source = "login";
|
|
723
|
-
}
|
|
776
|
+
const source = credentialSource(parsed.flags) ?? "none";
|
|
724
777
|
out({ authenticated: source !== "none", source, credentials: existsSync(credsPath()) ? credsPath() : null });
|
|
725
778
|
await flushExit(source === "none" ? 1 : 0);
|
|
726
779
|
}
|
|
@@ -739,18 +792,7 @@ function configFilePath(): string {
|
|
|
739
792
|
return join(configDir(), "config.json");
|
|
740
793
|
}
|
|
741
794
|
|
|
742
|
-
function readConfig(): CliConfigFile {
|
|
743
|
-
try {
|
|
744
|
-
return JSON.parse(readFileSync(configFilePath(), "utf8")) as CliConfigFile;
|
|
745
|
-
} catch {
|
|
746
|
-
return {};
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function writeConfig(config: CliConfigFile): void {
|
|
751
|
-
mkdirSync(configDir(), { recursive: true, mode: 0o700 });
|
|
752
|
-
writeFileSync(configFilePath(), JSON.stringify(config, null, 2) + "\n");
|
|
753
|
-
}
|
|
795
|
+
function readConfig(): CliConfigFile { return readProfileConfig(configDir()); }
|
|
754
796
|
|
|
755
797
|
const CONFIG_KEYS = ["base-url", "environment", "docs-url"];
|
|
756
798
|
|
|
@@ -784,6 +826,7 @@ async function cmdConfig(parsed: Parsed): Promise<void> {
|
|
|
784
826
|
if (sub === undefined || sub === "list") {
|
|
785
827
|
const config = readConfig();
|
|
786
828
|
out({
|
|
829
|
+
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
787
830
|
base_url: config.baseUrl ?? null,
|
|
788
831
|
environment: config.environment ?? null,
|
|
789
832
|
docs_url: config.docsUrl ?? DOCS_URL_DEFAULT,
|
|
@@ -806,10 +849,13 @@ async function cmdConfig(parsed: Parsed): Promise<void> {
|
|
|
806
849
|
await flushExit(0);
|
|
807
850
|
}
|
|
808
851
|
if (sub === "unset") {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
852
|
+
await updateProfileConfig(configDir(), (current) => {
|
|
853
|
+
const next = { ...current };
|
|
854
|
+
if (key === "base-url") delete next.baseUrl;
|
|
855
|
+
else if (key === "environment") delete next.environment;
|
|
856
|
+
else delete next.docsUrl;
|
|
857
|
+
return next;
|
|
858
|
+
});
|
|
813
859
|
out({ ok: true });
|
|
814
860
|
await flushExit(0);
|
|
815
861
|
}
|
|
@@ -820,17 +866,14 @@ async function cmdConfig(parsed: Parsed): Promise<void> {
|
|
|
820
866
|
} catch {
|
|
821
867
|
fail(2, key + " must be a valid URL");
|
|
822
868
|
}
|
|
823
|
-
if (key === "base-url") config.baseUrl = value;
|
|
824
|
-
else config.docsUrl = value;
|
|
825
869
|
} else {
|
|
826
870
|
if (!(value! in ENVIRONMENTS)) {
|
|
827
871
|
fail(2, Object.keys(ENVIRONMENTS).length > 0
|
|
828
872
|
? "environment must be one of: " + Object.keys(ENVIRONMENTS).join(", ")
|
|
829
873
|
: "The spec declares no named environments; use 'config set base-url' instead.");
|
|
830
874
|
}
|
|
831
|
-
config.environment = value;
|
|
832
875
|
}
|
|
833
|
-
|
|
876
|
+
await updateProfileConfig(configDir(), (current) => key === "base-url" ? { ...current, baseUrl: value } : key === "docs-url" ? { ...current, docsUrl: value } : { ...current, environment: value, baseUrl: undefined });
|
|
834
877
|
out({ ok: true, [key.replace(/-/g, "_")]: value });
|
|
835
878
|
await flushExit(0);
|
|
836
879
|
}
|
|
@@ -873,7 +916,12 @@ function mcpEntryFor(url: string | undefined, readOnly = false): { entry: McpEnt
|
|
|
873
916
|
}
|
|
874
917
|
const server = mcpServerPath();
|
|
875
918
|
if (server.warning) warnings.push(server.warning);
|
|
876
|
-
|
|
919
|
+
const environment = {
|
|
920
|
+
...(process.env.XDG_CONFIG_HOME !== undefined ? { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME } : {}),
|
|
921
|
+
...(process.env["TYPESHIP_CREDENTIAL_STORE"] !== undefined ? { "TYPESHIP_CREDENTIAL_STORE": process.env["TYPESHIP_CREDENTIAL_STORE"]! } : {}),
|
|
922
|
+
};
|
|
923
|
+
const pinned = PROFILE.source === "flag" || PROFILE.source === "environment";
|
|
924
|
+
return { entry: { command: "node", args: [server.path, ...(pinned ? ["--profile", PROFILE.name] : []), ...(readOnly ? ["--read-only"] : [])], ...(Object.keys(environment).length ? { env: environment } : {}) }, warnings };
|
|
877
925
|
}
|
|
878
926
|
|
|
879
927
|
const MCP_CLIENT_FLAGS: Record<string, string> = {
|
|
@@ -970,7 +1018,7 @@ function agentContext(): AgentContext {
|
|
|
970
1018
|
apiTitle: API_TITLE,
|
|
971
1019
|
version: VERSION,
|
|
972
1020
|
envPrefix: ENV_PREFIX,
|
|
973
|
-
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : [])],
|
|
1021
|
+
authEnvVars: [...AUTH_SCALARS.map((a) => a.env), ...(BASIC ? [BASIC.envUser, BASIC.envPass] : []), ...(Object.keys(NAMED_SCHEMES).length ? ["TYPESHIP_CREDENTIALS"] : [])],
|
|
974
1022
|
docsUrl: docsSiteUrl(),
|
|
975
1023
|
docsIndexUrl: docsIndexUrl(),
|
|
976
1024
|
generatedOperationCount: OPS.length,
|
|
@@ -1049,7 +1097,7 @@ function helpJson(): Record<string, unknown> {
|
|
|
1049
1097
|
note: "Choose an operation from this index, then read only that operation's complete schemas and example arguments.",
|
|
1050
1098
|
},
|
|
1051
1099
|
builtins: BUILTIN_COMMANDS,
|
|
1052
|
-
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>")],
|
|
1100
|
+
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>")],
|
|
1053
1101
|
auth_env_vars: agentContext().authEnvVars,
|
|
1054
1102
|
};
|
|
1055
1103
|
}
|
|
@@ -1066,29 +1114,44 @@ async function cmdAgentGuide(parsed: Parsed): Promise<void> {
|
|
|
1066
1114
|
/** Which credential the CLI would send, without sending it. --live calls the identity endpoint too. */
|
|
1067
1115
|
async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
1068
1116
|
const sub = parsed.positionals[1];
|
|
1117
|
+
if (!parsed.help && sub === "profiles") {
|
|
1118
|
+
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 })) });
|
|
1119
|
+
await flushExit(0);
|
|
1120
|
+
}
|
|
1121
|
+
if (!parsed.help && (sub === "use" || sub === "remove")) {
|
|
1122
|
+
const name = parsed.positionals[2];
|
|
1123
|
+
if (!name) fail(2, "auth " + sub + " requires a profile name.");
|
|
1124
|
+
if (sub === "use") selectProfile(configRoot(), name!);
|
|
1125
|
+
else await removeProfile(configRoot(), name!);
|
|
1126
|
+
const effective = resolveProfile(configRoot(), { flag: typeof parsed.flags.get("profile") === "string" ? parsed.flags.get("profile") as string : undefined, environment: process.env["TYPESHIP_PROFILE"], allowMissing: true });
|
|
1127
|
+
out({ ok: true, ...(sub === "use" ? { selected: name } : { removed: name }), profile: effective.name, source: effective.source });
|
|
1128
|
+
await flushExit(0);
|
|
1129
|
+
}
|
|
1069
1130
|
if (parsed.help || sub !== "check") {
|
|
1070
1131
|
process.stdout.write([
|
|
1071
1132
|
BIN + " auth check [--live] — report the credential the CLI would use, as JSON: {status: ok|action_required, authenticated, source, ...}",
|
|
1133
|
+
" " + BIN + " auth profiles list profiles without unlocking credentials",
|
|
1134
|
+
" " + BIN + " auth use <name> select the default profile",
|
|
1135
|
+
" " + BIN + " auth remove <name> remove a profile after logout",
|
|
1136
|
+
" --profile <name> overrides TYPESHIP_PROFILE, then the saved selection, then default.",
|
|
1072
1137
|
" --live also call the API's identity endpoint" + (WHOAMI ? "" : " (none in this API; --live is a no-op)"),
|
|
1073
1138
|
"",
|
|
1074
1139
|
"Precedence: flags > env vars > stored credentials (" + credsPath() + ").",
|
|
1075
1140
|
].join("\n") + "\n");
|
|
1076
1141
|
await flushExit(parsed.help ? 0 : 2);
|
|
1077
1142
|
}
|
|
1078
|
-
const
|
|
1079
|
-
let source = "none";
|
|
1080
|
-
if (AUTH_SCALARS.some((a) => typeof parsed.flags.get(a.flag) === "string")) source = "flags";
|
|
1081
|
-
else {
|
|
1082
|
-
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
1083
|
-
if (envScalar) source = "env:" + envScalar.env;
|
|
1084
|
-
else if (stored && (stored.scalars || stored.basic || stored.oauth)) source = "login";
|
|
1085
|
-
}
|
|
1143
|
+
const source = credentialSource(parsed.flags) ?? "none";
|
|
1086
1144
|
const authenticated = source !== "none";
|
|
1145
|
+
const savedIdentity = source === "login" ? readCreds() : null;
|
|
1146
|
+
if (savedIdentity) assertStoredIdentity(savedIdentity, identityConfiguration());
|
|
1087
1147
|
const report: Record<string, unknown> = {
|
|
1088
1148
|
status: authenticated ? "ok" : "action_required",
|
|
1089
1149
|
authenticated,
|
|
1090
1150
|
source,
|
|
1091
1151
|
credentials_path: existsSync(credsPath()) ? credsPath() : null,
|
|
1152
|
+
credential_storage: credentialStore().backend,
|
|
1153
|
+
verified_identity: savedIdentity?.identity?.values ?? null, identity_checked_at: savedIdentity?.identity?.checkedAt ?? null,
|
|
1154
|
+
profile: PROFILE.name, profile_source: PROFILE.source,
|
|
1092
1155
|
auth_env_vars: agentContext().authEnvVars,
|
|
1093
1156
|
base_url: resolveBaseUrl(parsed.flags) ?? null,
|
|
1094
1157
|
next_steps: authenticated ? [] : [
|
|
@@ -1099,10 +1162,13 @@ async function cmdAuth(parsed: Parsed): Promise<void> {
|
|
|
1099
1162
|
if (authenticated && parsed.flags.get("live") === true && WHOAMI) {
|
|
1100
1163
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1101
1164
|
if (op) {
|
|
1102
|
-
const client = await makeClient(parsed.flags);
|
|
1165
|
+
const client = await makeClient(parsed.flags, op);
|
|
1103
1166
|
const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; data?: unknown; error?: unknown }>>>)[op.resource]!;
|
|
1104
1167
|
const result = await target[op.method]!();
|
|
1105
|
-
if (result.ok)
|
|
1168
|
+
if (result.ok) {
|
|
1169
|
+
if (identityConfiguration() && savedIdentity?.identity) assertApiIdentity(savedIdentity.identity.values, readApiIdentity(result.data, IDENTITY_POLICY));
|
|
1170
|
+
report.identity = result.data;
|
|
1171
|
+
}
|
|
1106
1172
|
else {
|
|
1107
1173
|
const why = classifyApiError(result.error, { bin: BIN, hadCredential: true, docsUrl: DOCS_URL_DEFAULT });
|
|
1108
1174
|
report.status = "action_required";
|
|
@@ -1124,10 +1190,11 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1124
1190
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
1125
1191
|
checks.push({ name: "node", ok: nodeMajor >= 18, detail: process.version, ...(nodeMajor >= 18 ? {} : { fix: "Install Node 18 or newer." }) });
|
|
1126
1192
|
checks.push({ name: "cli", ok: true, detail: BIN + " " + VERSION + " (" + PKG_NAME + ")" });
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1193
|
+
let source: string | null = null;
|
|
1194
|
+
let storageProblem: string | undefined;
|
|
1195
|
+
try { source = credentialSource(parsed.flags); } catch (error) { storageProblem = (error as Error).message; }
|
|
1196
|
+
const hasCred = source !== null;
|
|
1197
|
+
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'.") }) });
|
|
1131
1198
|
const baseUrl = resolveBaseUrl(parsed.flags);
|
|
1132
1199
|
if (baseUrl) {
|
|
1133
1200
|
try {
|
|
@@ -1143,7 +1210,7 @@ async function cmdDoctor(parsed: Parsed): Promise<void> {
|
|
|
1143
1210
|
const op = OPS.find((o) => o.resource === WHOAMI.resource && o.method === WHOAMI.method);
|
|
1144
1211
|
if (op) {
|
|
1145
1212
|
try {
|
|
1146
|
-
const client = await makeClient(parsed.flags);
|
|
1213
|
+
const client = await makeClient(parsed.flags, op);
|
|
1147
1214
|
const target = (client as unknown as Record<string, Record<string, () => Promise<{ ok: boolean; error?: unknown }>>>)[op.resource]!;
|
|
1148
1215
|
const result = await target[op.method]!();
|
|
1149
1216
|
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." });
|
|
@@ -1196,24 +1263,33 @@ async function cmdInit(parsed: Parsed): Promise<void> {
|
|
|
1196
1263
|
const harness = detectHarness();
|
|
1197
1264
|
|
|
1198
1265
|
// 1. credential
|
|
1199
|
-
const stored: StoredCreds = readCreds() ?? {};
|
|
1200
1266
|
const first = AUTH_SCALARS[0];
|
|
1267
|
+
const named = flagCredentials(parsed.flags), envNamed = environmentCredentials();
|
|
1268
|
+
const stored: StoredCreds = Object.keys(named).length || Object.keys(envNamed).length || first && process.env[first.env] ? {} : readCreds() ?? {};
|
|
1201
1269
|
const given = (typeof parsed.flags.get("k") === "string" ? parsed.flags.get("k") as string : undefined)
|
|
1202
1270
|
?? (first && typeof parsed.flags.get(first.flag) === "string" ? parsed.flags.get(first.flag) as string : undefined);
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
|
|
1271
|
+
if (Object.keys(named).length) {
|
|
1272
|
+
await saveLoginCredentials({ named }, parsed.flags);
|
|
1273
|
+
report.credential = { status: "stored", path: credsPath() };
|
|
1274
|
+
} else if (given && first) {
|
|
1275
|
+
await saveLoginCredentials({ scalars: { [first.option]: given } }, parsed.flags);
|
|
1206
1276
|
report.credential = { status: "stored", path: credsPath() };
|
|
1207
1277
|
} else if (first && process.env[first.env]) {
|
|
1208
1278
|
report.credential = { status: "env", variable: first.env };
|
|
1209
|
-
} else if (
|
|
1279
|
+
} else if (Object.keys(envNamed).length) {
|
|
1280
|
+
report.credential = { status: "env", variable: "TYPESHIP_CREDENTIALS" };
|
|
1281
|
+
} else if (stored.scalars || stored.basic || stored.oauth || stored.named) {
|
|
1210
1282
|
report.credential = { status: "stored", path: credsPath() };
|
|
1283
|
+
} else if (first && HAS_OAUTH_LOGIN && OAUTH_LOGIN_METHOD === "browser" && (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID) && !explicitNonInteractive(parsed)) {
|
|
1284
|
+
await acquireOAuthBrowserSession(parsed, (process.env[ENV_PREFIX + "_CLIENT_ID"] ?? OAUTH_CLIENT_ID)!);
|
|
1285
|
+
report.credential = { status: "stored", method: "oauth_browser", path: credsPath() };
|
|
1211
1286
|
} else if (first && CLI_AUTH_URL && !explicitNonInteractive(parsed)) {
|
|
1212
1287
|
// Nothing anywhere: approve a credential in the browser, as `login`
|
|
1213
1288
|
// would, then carry on. Under an agent the URL is printed for the person
|
|
1214
1289
|
// and polled; only the explicit non-interactive switch skips this.
|
|
1290
|
+
await credentialStore().prepare();
|
|
1215
1291
|
const minted = await browserApprove(isAgentMode(parsed) || parsed.flags.get("no-browser") === true, parsed.flags);
|
|
1216
|
-
storeMinted(
|
|
1292
|
+
await storeMinted(minted, parsed.flags);
|
|
1217
1293
|
report.credential = { status: "minted", method: "browser", key_name: minted.key_name, ...(minted.org_id ? { org_id: minted.org_id } : {}), path: credsPath() };
|
|
1218
1294
|
} else {
|
|
1219
1295
|
report.credential = { status: "none" };
|
|
@@ -1348,7 +1424,7 @@ function completionFlagsFor(op: OpSpec): { flags: string[]; values: Record<strin
|
|
|
1348
1424
|
return { flags, values };
|
|
1349
1425
|
}
|
|
1350
1426
|
|
|
1351
|
-
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)];
|
|
1427
|
+
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)];
|
|
1352
1428
|
const BUILTIN_WORDS: Record<string, string[]> = {
|
|
1353
1429
|
config: ["list", "get", "set", "unset", "path"],
|
|
1354
1430
|
completion: ["bash", "zsh", "fish"],
|
|
@@ -1608,30 +1684,7 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1608
1684
|
.filter((match) => match.score > 0)
|
|
1609
1685
|
.sort((a, b) => b.score - a.score || a.op.command.join(" ").localeCompare(b.op.command.join(" ")))
|
|
1610
1686
|
.map((match) => match.op);
|
|
1611
|
-
const
|
|
1612
|
-
const proseMatches: { heading: string; excerpt: string; score: number }[] = [];
|
|
1613
|
-
if (prose !== null) {
|
|
1614
|
-
let heading = "";
|
|
1615
|
-
const terms = searchTerms(term);
|
|
1616
|
-
for (const line of prose.split("\n")) {
|
|
1617
|
-
if (/^#{1,3} /.test(line)) heading = line.replace(/^#+ /, "").trim();
|
|
1618
|
-
else {
|
|
1619
|
-
const lowerHeading = heading.toLowerCase();
|
|
1620
|
-
const lowerLine = line.toLowerCase();
|
|
1621
|
-
const matched = terms.filter((word) => lowerHeading.includes(word) || lowerLine.includes(word));
|
|
1622
|
-
if (matched.length > 0) {
|
|
1623
|
-
const allTerms = matched.length === terms.length;
|
|
1624
|
-
proseMatches.push({
|
|
1625
|
-
heading,
|
|
1626
|
-
excerpt: line.trim().slice(0, 160),
|
|
1627
|
-
score: matched.length * 10 + (allTerms ? 50 : 0) + (lowerHeading.includes(term.toLowerCase()) || lowerLine.includes(term.toLowerCase()) ? 25 : 0),
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
proseMatches.sort((a, b) => b.score - a.score || a.heading.localeCompare(b.heading) || a.excerpt.localeCompare(b.excerpt));
|
|
1633
|
-
}
|
|
1634
|
-
const docsStatus = docsSiteUrl() === null ? "not_configured" : prose === null ? "unavailable" : "ok";
|
|
1687
|
+
const { guides: proseMatches, status: docsStatus } = await searchConnectedGuides(docsSiteUrl(), docsIndexUrl(), fetchDocs, term);
|
|
1635
1688
|
if (jsonOutput) {
|
|
1636
1689
|
out({
|
|
1637
1690
|
schema_version: "1",
|
|
@@ -1643,7 +1696,7 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1643
1696
|
...(op.summary ? { summary: op.summary } : {}),
|
|
1644
1697
|
details_command: BIN + " docs " + op.command.join(" ") + " --json",
|
|
1645
1698
|
})),
|
|
1646
|
-
guides: proseMatches.slice(0, 15).map((
|
|
1699
|
+
guides: proseMatches.slice(0, 15).map((match) => ({ ...match, read_command: docsReadCommand(BIN, match.url) })),
|
|
1647
1700
|
totals: { reference: refMatches.length, guides: proseMatches.length },
|
|
1648
1701
|
guides_status: docsStatus,
|
|
1649
1702
|
...(docsStatus === "not_configured" ? { next_steps: ["Run '" + BIN + " config set docs-url <url>' to add guide search; the API reference was still searched."] } : {}),
|
|
@@ -1658,7 +1711,7 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1658
1711
|
}
|
|
1659
1712
|
if (proseMatches.length > 0) {
|
|
1660
1713
|
lines.push(...(lines.length > 0 ? [""] : []), paintOut("bold", "Guides:"));
|
|
1661
|
-
for (const match of proseMatches.slice(0, 15)) lines.push(" " +
|
|
1714
|
+
for (const match of proseMatches.slice(0, 15)) lines.push(" " + paintOut("cyan", match.title + (match.section ? " / " + match.section : "")), " " + match.excerpt, " " + docsReadCommand(BIN, match.url));
|
|
1662
1715
|
} else if (docsStatus === "not_configured") {
|
|
1663
1716
|
lines.push(...(lines.length > 0 ? [""] : []), "(no docs site configured for guide search: '" + BIN + " config set docs-url <url>')");
|
|
1664
1717
|
} else if (docsStatus === "unavailable") {
|
|
@@ -1675,9 +1728,7 @@ async function cmdDocs(parsed: Parsed): Promise<void> {
|
|
|
1675
1728
|
let target = page!;
|
|
1676
1729
|
if (!/^https?:\/\//.test(target)) {
|
|
1677
1730
|
const index = await fetchDocs("llms.txt");
|
|
1678
|
-
|
|
1679
|
-
const hit = linked.find((u) => u.toLowerCase().includes(target.toLowerCase()));
|
|
1680
|
-
if (hit !== undefined) target = hit;
|
|
1731
|
+
target = docsReadTarget(index, docsSiteUrl(), docsIndexUrl(), target);
|
|
1681
1732
|
}
|
|
1682
1733
|
const text = await fetchDocs(target);
|
|
1683
1734
|
if (text === null) {
|
|
@@ -1893,7 +1944,7 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
1893
1944
|
}
|
|
1894
1945
|
const width = termWidth();
|
|
1895
1946
|
const lines: string[] = [];
|
|
1896
|
-
lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.
|
|
1947
|
+
lines.push(paintOut("bold", BIN) + ": " + "typeship API" + " (v" + "1.0.0" + "), package " + "0.10.0");
|
|
1897
1948
|
lines.push("");
|
|
1898
1949
|
lines.push(paintOut("bold", "Usage:") + " " + BIN + " <resource> <command> [args] [--flags]");
|
|
1899
1950
|
lines.push("");
|
|
@@ -1917,11 +1968,11 @@ function printRoot(stream: NodeJS.WriteStream = process.stdout): void {
|
|
|
1917
1968
|
lines.push(...labeled("Upgrade: ", "https://typeship.dev/pricing, then regenerate without the operation cap", width, 14));
|
|
1918
1969
|
}
|
|
1919
1970
|
lines.push("");
|
|
1920
|
-
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)" +
|
|
1971
|
+
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)" +
|
|
1921
1972
|
(AUTH_SCALARS.length > 0 ? ", " + AUTH_SCALARS.map((a) => "--" + a.flag + " <value>").join(", ") : "");
|
|
1922
1973
|
lines.push(...labeled(paintOut("bold", "Global flags:") + " ", flagsText, width, 14).map((l, i) => (i === 0 ? l : l)));
|
|
1923
1974
|
lines.push(...labeled("Credential env vars: ", [
|
|
1924
|
-
...AUTH_SCALARS.map((a) => a.env),
|
|
1975
|
+
"TYPESHIP_CREDENTIALS", ...AUTH_SCALARS.map((a) => a.env),
|
|
1925
1976
|
...(BASIC ? [BASIC.envUser, BASIC.envPass] : []),
|
|
1926
1977
|
].join(", ") || "none", width, 21));
|
|
1927
1978
|
lines.push(...labeled("Endpoint env var: ", "TYPESHIP_BASE_URL", width, 18));
|
|
@@ -1963,6 +2014,7 @@ function printResource(resource: string, stream: NodeJS.WriteStream = process.st
|
|
|
1963
2014
|
/** Flags the CLI itself adds to an API command, as [flag, description] rows. */
|
|
1964
2015
|
function commandExtras(op: OpSpec): [string, string][] {
|
|
1965
2016
|
const extras: [string, string][] = [];
|
|
2017
|
+
if (op.auth !== "none" && Object.keys(NAMED_SCHEMES).length) extras.push(["--credentials @<file>|-", "named credentials as JSON; use - for stdin"]);
|
|
1966
2018
|
if (op.hasBody && op.bodyKind === "binary") extras.push(["--file <path>", "raw request body, uploaded as-is (- reads stdin)"]);
|
|
1967
2019
|
else if (op.hasBody) extras.push(["--data '<json>'", "raw JSON body" + (op.bodyStyle === "fields" ? " (merged under field flags)" : "") + "; @<file> reads a file, - reads stdin"]);
|
|
1968
2020
|
if (op.select) extras.push(["--select '<selection>'", "GraphQL selection set replacing the default, e.g. '{ id name }'"]);
|
|
@@ -2006,7 +2058,7 @@ function exampleLine(op: OpSpec): string {
|
|
|
2006
2058
|
|
|
2007
2059
|
/** " (no auth needed)" for an anonymous operation in an API that otherwise authenticates. */
|
|
2008
2060
|
function authNote(op: OpSpec): string {
|
|
2009
|
-
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null ||
|
|
2061
|
+
const apiHasAuth = AUTH_SCALARS.length > 0 || BASIC !== null || HAS_OAUTH_LOGIN;
|
|
2010
2062
|
return apiHasAuth && op.auth === "none" ? " (no auth needed)" : apiHasAuth && op.auth === "optional" ? " (auth optional)" : "";
|
|
2011
2063
|
}
|
|
2012
2064
|
|
|
@@ -2158,21 +2210,48 @@ function coerce(spec: ParamSpec, raw: string | boolean, repeated?: string[]): un
|
|
|
2158
2210
|
/** Whether the last client built carried any credential; failApi tells NO_AUTH from AUTH_INVALID with it. */
|
|
2159
2211
|
let LAST_CLIENT_HAD_CREDENTIAL = false;
|
|
2160
2212
|
|
|
2213
|
+
function validateCredentialsInput(flags: Map<string, string | boolean>): void {
|
|
2214
|
+
const input = flags.get("credentials");
|
|
2215
|
+
if (BASIC && flags.has("username") !== flags.has("password")) fail(2, "Supply --username and --password together, or use a complete named Basic credential.");
|
|
2216
|
+
if (input === undefined) return;
|
|
2217
|
+
if (typeof input !== "string" || (input !== "-" && (!input.startsWith("@") || input.length < 2))) fail(2, "--credentials expects @<JSON-file> or - to read JSON from stdin.");
|
|
2218
|
+
if (input === "-" && (flags.get("data") === "-" || flags.get("file") === "-" || flags.get("with-token") === true)) fail(2, "Stdin cannot supply both named credentials and another input. Use a credential file instead.");
|
|
2219
|
+
}
|
|
2220
|
+
let namedFlagCache: NamedCredentials | undefined;
|
|
2221
|
+
function flagCredentials(flags: Map<string, string | boolean>): NamedCredentials {
|
|
2222
|
+
if (namedFlagCache) return namedFlagCache;
|
|
2223
|
+
validateCredentialsInput(flags);
|
|
2224
|
+
const input = flags.get("credentials");
|
|
2225
|
+
if (input === undefined) return {};
|
|
2226
|
+
try {
|
|
2227
|
+
const named = readNamedCredentialsFile(input as string, NAMED_SCHEMES);
|
|
2228
|
+
if (!Object.keys(named).length) fail(2, "The named credential file must contain at least one security scheme.");
|
|
2229
|
+
return namedFlagCache = named;
|
|
2230
|
+
}
|
|
2231
|
+
catch (error) { fail(2, (error as Error).message); }
|
|
2232
|
+
}
|
|
2233
|
+
function environmentCredentials(): NamedCredentials {
|
|
2234
|
+
if (BASIC && (process.env[BASIC.envUser] !== undefined) !== (process.env[BASIC.envPass] !== undefined)) fail(2, "Supply both Basic-auth environment variables together, or use a complete named Basic credential.");
|
|
2235
|
+
const value = process.env["TYPESHIP_CREDENTIALS"];
|
|
2236
|
+
return value === undefined ? {} : parseNamedCredentials(value, NAMED_SCHEMES);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2161
2239
|
/** Where a credential would come from, without sending it: "flags", "env:<VAR>", "login", or null. */
|
|
2162
2240
|
function credentialSource(flags: Map<string, string | boolean>): string | null {
|
|
2241
|
+
if (Object.keys(flagCredentials(flags)).length) return "flags";
|
|
2163
2242
|
if (AUTH_SCALARS.some((a) => typeof flags.get(a.flag) === "string")) return "flags";
|
|
2164
2243
|
if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string") return "flags";
|
|
2244
|
+
if (Object.keys(environmentCredentials()).length) return "env:TYPESHIP_CREDENTIALS";
|
|
2165
2245
|
const envScalar = AUTH_SCALARS.find((a) => process.env[a.env] !== undefined);
|
|
2166
2246
|
if (envScalar) return "env:" + envScalar.env;
|
|
2167
2247
|
if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined) return "env:" + BASIC.envUser;
|
|
2168
2248
|
const stored = readCreds();
|
|
2169
|
-
if (stored && (stored.scalars || stored.basic || stored.oauth)) return "login";
|
|
2249
|
+
if (stored && (stored.scalars || stored.basic || stored.oauth || stored.named)) return "login";
|
|
2170
2250
|
return null;
|
|
2171
2251
|
}
|
|
2172
2252
|
|
|
2173
2253
|
/** Base URL resolution: --base-url > env > config base-url > config environment > spec default. */
|
|
2174
|
-
function resolveBaseUrl(flags: Map<string, string | boolean
|
|
2175
|
-
const config = readConfig();
|
|
2254
|
+
function resolveBaseUrl(flags: Map<string, string | boolean>, config = readConfig()): string | undefined {
|
|
2176
2255
|
return (typeof flags.get("base-url") === "string" ? flags.get("base-url") as string : undefined)
|
|
2177
2256
|
?? process.env["TYPESHIP_BASE_URL"]
|
|
2178
2257
|
?? config.baseUrl
|
|
@@ -2180,10 +2259,22 @@ function resolveBaseUrl(flags: Map<string, string | boolean>): string | undefine
|
|
|
2180
2259
|
?? DEFAULT_BASE_URL ?? undefined;
|
|
2181
2260
|
}
|
|
2182
2261
|
|
|
2183
|
-
async function makeClient(flags: Map<string, string | boolean
|
|
2184
|
-
const
|
|
2185
|
-
const
|
|
2262
|
+
async function makeClient(flags: Map<string, string | boolean>, op: OpSpec, candidate?: StoredCreds, forIdentity = false): Promise<TypeshipClient> {
|
|
2263
|
+
const flagNamed = flagCredentials(flags), envNamed = environmentCredentials();
|
|
2264
|
+
const explicitOptions = new Set(AUTH_SCALARS.filter((a) => typeof flags.get(a.flag) === "string" || process.env[a.env] !== undefined).map((a) => a.option));
|
|
2265
|
+
if (BASIC && (typeof flags.get("username") === "string" || process.env[BASIC.envUser] !== undefined) && (typeof flags.get("password") === "string" || process.env[BASIC.envPass] !== undefined)) explicitOptions.add("basicAuth");
|
|
2266
|
+
namedCredentialAvailability(NAMED_SCHEMES, explicitOptions, { ...envNamed, ...flagNamed });
|
|
2267
|
+
const explicitCredentials = op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => explicitOptions.has(option)));
|
|
2268
|
+
// A complete explicit alternative for this endpoint does not unlock or mix
|
|
2269
|
+
// in saved credentials for another API, account, or authentication method.
|
|
2270
|
+
const stored = candidate ?? (op.auth === "none" || explicitCredentials ? null : readCreds());
|
|
2271
|
+
const config = readConfig();
|
|
2272
|
+
const baseUrl = resolveBaseUrl(flags, config);
|
|
2186
2273
|
if (baseUrl === undefined) fail(2, "No base URL. Pass --base-url, set TYPESHIP_BASE_URL, or run '" + BIN + " config set base-url <url>'.");
|
|
2274
|
+
if (stored) {
|
|
2275
|
+
assertCredentialDestination(stored, { apiBaseUrl: baseUrl, environment: config.environment, profile: PROFILE.name });
|
|
2276
|
+
if (!forIdentity) assertStoredIdentity(stored, identityConfiguration());
|
|
2277
|
+
}
|
|
2187
2278
|
const options: ClientOptions & Record<string, unknown> = { baseUrl };
|
|
2188
2279
|
for (const a of AUTH_SCALARS) {
|
|
2189
2280
|
const v = (typeof flags.get(a.flag) === "string" ? flags.get(a.flag) as string : undefined)
|
|
@@ -2197,9 +2288,24 @@ async function makeClient(flags: Map<string, string | boolean>): Promise<Typeshi
|
|
|
2197
2288
|
?? process.env[BASIC.envPass] ?? stored?.basic?.password;
|
|
2198
2289
|
if (username !== undefined && password !== undefined) options.basicAuth = { username, password };
|
|
2199
2290
|
}
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
if (
|
|
2291
|
+
const envOptions: Record<string, unknown> = {}, flagOptions: Record<string, unknown> = {};
|
|
2292
|
+
for (const a of AUTH_SCALARS) {
|
|
2293
|
+
if (process.env[a.env] !== undefined) envOptions[a.option] = process.env[a.env];
|
|
2294
|
+
if (typeof flags.get(a.flag) === "string") flagOptions[a.option] = flags.get(a.flag);
|
|
2295
|
+
}
|
|
2296
|
+
if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined) envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
|
|
2297
|
+
if (BASIC && typeof flags.get("username") === "string" && typeof flags.get("password") === "string") flagOptions.basicAuth = { username: flags.get("username"), password: flags.get("password") };
|
|
2298
|
+
options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
|
|
2299
|
+
{ named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
|
|
2300
|
+
{ named: envNamed, options: envOptions }, { named: flagNamed, options: flagOptions },
|
|
2301
|
+
...(forIdentity && candidate ? [{ named: candidate.named, options: { ...candidate.scalars, ...(candidate.basic ? { basicAuth: candidate.basic } : {}), ...(candidate.oauth ? { bearerToken: candidate.oauth.accessToken } : {}) } }] : []),
|
|
2302
|
+
]);
|
|
2303
|
+
if (stored?.oauth && (forIdentity || options.bearerToken === undefined)) {
|
|
2304
|
+
const sessionId = stored.oauth.sessionId;
|
|
2305
|
+
options.bearerToken = forIdentity ? stored.oauth.accessToken : () => oauthSessionToken(credentialStore(), {
|
|
2306
|
+
...sessionConfiguration(baseUrl, config),
|
|
2307
|
+
...(identityConfiguration() && WHOAMI ? { verifyIdentity: (accessToken: string) => verifyClientIdentity((values) => new TypeshipClient(values as unknown as ClientOptions), { ...options, bearerToken: accessToken, credentials: resolveNamedCredentials(NAMED_SCHEMES, [{ named: options.credentials as NamedCredentials }, { options: { bearerToken: accessToken } }]) }, WHOAMI!, IDENTITY_POLICY) } : {}),
|
|
2308
|
+
}, sessionId, OAUTH_TOKEN_PARAMS);
|
|
2203
2309
|
}
|
|
2204
2310
|
if (flags.get("debug") === true || process.env["TYPESHIP_DEBUG"] === "1") {
|
|
2205
2311
|
options.debug = (event: DebugEvent) => process.stderr.write(paintErr("dim", formatDebugEvent(BIN, event)) + "\n");
|
|
@@ -2210,7 +2316,7 @@ async function makeClient(flags: Map<string, string | boolean>): Promise<Typeshi
|
|
|
2210
2316
|
const value = typeof flagValue === "string" ? flagValue : process.env["TYPESHIP_" + g.envSuffix];
|
|
2211
2317
|
if (value !== undefined) options[g.option] = value;
|
|
2212
2318
|
}
|
|
2213
|
-
LAST_CLIENT_HAD_CREDENTIAL = AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2319
|
+
LAST_CLIENT_HAD_CREDENTIAL = Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined;
|
|
2214
2320
|
// Who is calling: the CLI, under which agent harness, and whether an
|
|
2215
2321
|
// agent is driving. "agent" means a harness was detected or the caller
|
|
2216
2322
|
// said so (--mode agent / env); a bare non-TTY run (CI, a pipeline) is
|
|
@@ -2224,6 +2330,7 @@ async function makeClient(flags: Map<string, string | boolean>): Promise<Typeshi
|
|
|
2224
2330
|
...(options.defaultHeaders as Record<string, string> | undefined),
|
|
2225
2331
|
"User-Agent": PKG_NAME + "-cli/" + VERSION + " (typeship" + (harness ? "; harness=" + harness : "") + caller + ")",
|
|
2226
2332
|
};
|
|
2333
|
+
if (forIdentity) { options.fetch = identityFetch(baseUrl); options.maxRetries = 0; options.timeoutMs = 10_000; }
|
|
2227
2334
|
return new TypeshipClient(options);
|
|
2228
2335
|
}
|
|
2229
2336
|
|
|
@@ -2272,6 +2379,11 @@ const BUILTIN_COMMANDS = ["login","logout","whoami","config","mcp","docs","upgra
|
|
|
2272
2379
|
async function main(): Promise<void> {
|
|
2273
2380
|
const argv = process.argv.slice(2);
|
|
2274
2381
|
const parsed = parseArgv(argv);
|
|
2382
|
+
if (!parsed.help) validateCredentialsInput(parsed.flags);
|
|
2383
|
+
const profileFlag = parsed.flags.get("profile");
|
|
2384
|
+
if (profileFlag !== undefined && typeof profileFlag !== "string") fail(2, "--profile requires a profile name.");
|
|
2385
|
+
PROFILE = resolveProfile(configRoot(), { flag: profileFlag as string | undefined, environment: process.env["TYPESHIP_PROFILE"], allowMissing: ["login", "config", "auth", "init", "help"].includes(parsed.positionals[0] ?? "") || parsed.help });
|
|
2386
|
+
if (!parsed.help && ["login", "init"].includes(parsed.positionals[0] ?? "")) expectedLoginIdentity(parsed.flags);
|
|
2275
2387
|
if (parsed.positionals[0] === "help") {
|
|
2276
2388
|
// help --json: the command surface as data (agents read this once).
|
|
2277
2389
|
if (parsed.flags.get("json") === true || parsed.flags.get("format") === "json") { out(helpJson()); await flushExit(0); }
|
|
@@ -2309,8 +2421,8 @@ async function main(): Promise<void> {
|
|
|
2309
2421
|
if (resourceCmd === "mcp") { await cmdMcp(parsed); }
|
|
2310
2422
|
if (resourceCmd === "login") { await cmdLogin(parsed); }
|
|
2311
2423
|
if (resourceCmd === "logout") {
|
|
2312
|
-
if (parsed.help) { process.stdout.write(BIN + " logout — remove " + credsPath() + "\n"); await flushExit(0); }
|
|
2313
|
-
await cmdLogout();
|
|
2424
|
+
if (parsed.help) { process.stdout.write(BIN + " logout — remove " + credsPath() + "\n --local remove the local session without unlocking OS storage or revoking tokens\n"); await flushExit(0); }
|
|
2425
|
+
await cmdLogout(parsed);
|
|
2314
2426
|
}
|
|
2315
2427
|
if (resourceCmd === "whoami") {
|
|
2316
2428
|
if (parsed.help) {
|
|
@@ -2374,7 +2486,7 @@ async function main(): Promise<void> {
|
|
|
2374
2486
|
// Mirrors opReservedFlags() in the generator: API parameters never use these
|
|
2375
2487
|
// names (colliding ones are emitted as --<kind>-<name>), so an unknown flag
|
|
2376
2488
|
// check can be exact.
|
|
2377
|
-
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)]);
|
|
2489
|
+
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)]);
|
|
2378
2490
|
for (const spec of op.params) {
|
|
2379
2491
|
if (spec.kind === "path") continue;
|
|
2380
2492
|
const raw = parsed.flags.get(spec.flag);
|
|
@@ -2429,7 +2541,7 @@ async function main(): Promise<void> {
|
|
|
2429
2541
|
|
|
2430
2542
|
// The spec says this operation needs a credential and none resolved:
|
|
2431
2543
|
// say so now, locally, instead of sending a request to learn it.
|
|
2432
|
-
if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC ||
|
|
2544
|
+
if (op.auth === "required" && (AUTH_SCALARS.length > 0 || BASIC || HAS_OAUTH_LOGIN) && credentialSource(parsed.flags) === null) {
|
|
2433
2545
|
failWith({
|
|
2434
2546
|
status: "action_required",
|
|
2435
2547
|
code: "NO_AUTH",
|
|
@@ -2461,7 +2573,7 @@ async function main(): Promise<void> {
|
|
|
2461
2573
|
if (answer !== "y" && answer !== "yes") failWith({ status: "action_required", code: "CONFIRMATION_REQUIRED", message: "Cancelled.", nextSteps: ["Run again with --force to skip the prompt: " + rerun] });
|
|
2462
2574
|
}
|
|
2463
2575
|
|
|
2464
|
-
const client = await makeClient(parsed.flags);
|
|
2576
|
+
const client = await makeClient(parsed.flags, op);
|
|
2465
2577
|
const selectValue = typeof parsed.flags.get("select") === "string" ? (parsed.flags.get("select") as string) : undefined;
|
|
2466
2578
|
const args = buildArgs(op, values, dataBody, selectValue);
|
|
2467
2579
|
const target = (client as unknown as Record<string, Record<string, (...a: unknown[]) => unknown>>)[op.resource]!;
|