linear-sdk-cli 0.7.2 → 0.8.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/CHANGELOG.md +16 -0
- package/README.md +62 -11
- package/package.json +2 -2
- package/skills/linear-sdk-cli/SKILL.md +38 -11
- package/skills/linear-sdk-cli/references/auth.md +15 -11
- package/src/bin/linear.ts +6 -7
- package/src/cli.ts +88 -12
- package/src/client.ts +46 -18
- package/src/commands/issue.ts +7 -1
- package/src/commands/meta.ts +113 -15
- package/src/config.ts +276 -19
- package/src/index.ts +2 -0
- package/src/lib/authorization.ts +7 -4
- package/src/lib/errors.ts +2 -0
- package/src/oauth.ts +32 -0
- package/src/output/format.ts +7 -2
package/src/commands/meta.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { withRetry } from "../client.js";
|
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import {
|
|
12
12
|
writeCredential,
|
|
13
|
+
writeAppCredential,
|
|
13
14
|
probeKeyringCredential,
|
|
14
15
|
adoptKeyringCredential,
|
|
15
16
|
removeCredentialWithMetadata,
|
|
@@ -43,6 +44,8 @@ import {
|
|
|
43
44
|
credentialType as resolvedCredentialType,
|
|
44
45
|
} from "../lib/authorization.js";
|
|
45
46
|
import {
|
|
47
|
+
ClientCredentialsTokenProvider,
|
|
48
|
+
clientCredentialsScopes,
|
|
46
49
|
DEFAULT_OAUTH_SCOPES,
|
|
47
50
|
buildAuthorizationUrl,
|
|
48
51
|
createPkceRequest,
|
|
@@ -229,12 +232,25 @@ export function registerMeta(program: Command): void {
|
|
|
229
232
|
.description("Authenticate and select the workspace for this project")
|
|
230
233
|
.option("--no-project", "save credentials without changing the project workspace")
|
|
231
234
|
.option("--key <key>", "use a personal API key ('-' reads it from stdin)")
|
|
232
|
-
.option(
|
|
235
|
+
.option(
|
|
236
|
+
"--plaintext",
|
|
237
|
+
"Store API-key or app credentials in the private user config file (0600)",
|
|
238
|
+
)
|
|
233
239
|
.option("--no-browser", "print the authorization URL instead of opening it")
|
|
234
240
|
.option("--read-only", "request read-only OAuth access")
|
|
235
241
|
.option("--admin", "explicitly add the OAuth admin scope")
|
|
236
242
|
.option("--timeout <seconds>", "seconds to wait for the loopback callback", "120")
|
|
237
|
-
.option("--
|
|
243
|
+
.option("--app", "sign in as an app using client credentials")
|
|
244
|
+
.option("--client-credentials", "alias for --app")
|
|
245
|
+
.option("--scope <scopes>", "comma-separated app scopes (default: read,write)")
|
|
246
|
+
.option(
|
|
247
|
+
"--client-secret <secret>",
|
|
248
|
+
"app client secret ('-' reads stdin; defaults to LINEAR_CLIENT_SECRET)",
|
|
249
|
+
)
|
|
250
|
+
.option(
|
|
251
|
+
"--client-id <id>",
|
|
252
|
+
"OAuth client ID (app: LINEAR_CLIENT_ID; browser: packaged CLI app)",
|
|
253
|
+
)
|
|
238
254
|
.option("--redirect-uri <uri>", "registered HTTP loopback callback URI")
|
|
239
255
|
.action(
|
|
240
256
|
action(async (ctx: Context, opts) => {
|
|
@@ -243,6 +259,84 @@ export function registerMeta(program: Command): void {
|
|
|
243
259
|
"`auth login` cannot persist an injected access token. Remove --access-token and use browser OAuth, or use --key - for a personal API key.",
|
|
244
260
|
);
|
|
245
261
|
}
|
|
262
|
+
if (opts.app || opts.clientCredentials) {
|
|
263
|
+
if (
|
|
264
|
+
opts.key !== undefined ||
|
|
265
|
+
ctx.options.apiKey !== undefined ||
|
|
266
|
+
opts.redirectUri ||
|
|
267
|
+
opts.browser === false
|
|
268
|
+
) {
|
|
269
|
+
throw usageError(
|
|
270
|
+
"--app cannot be combined with personal API-key or browser callback options.",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (opts.admin) throw usageError("App actors cannot request the admin scope.");
|
|
274
|
+
if (!opts.plaintext && !keyring())
|
|
275
|
+
throw new CliError(
|
|
276
|
+
"App authentication requires a system keyring; use --plaintext for explicit private-file storage.",
|
|
277
|
+
"runtime",
|
|
278
|
+
);
|
|
279
|
+
const clientId = String(
|
|
280
|
+
opts.clientId ??
|
|
281
|
+
process.env.LINEAR_CLIENT_ID ??
|
|
282
|
+
process.env.LINEAR_OAUTH_CLIENT_ID ??
|
|
283
|
+
"",
|
|
284
|
+
).trim();
|
|
285
|
+
let clientSecret = String(opts.clientSecret ?? process.env.LINEAR_CLIENT_SECRET ?? "");
|
|
286
|
+
if (clientSecret === "-") clientSecret = readStdinSync().trim();
|
|
287
|
+
else if (clientSecret && opts.clientSecret !== undefined)
|
|
288
|
+
ctx.output.warn(
|
|
289
|
+
"A client secret on the command line is visible in shell history; prefer --client-secret - (stdin).",
|
|
290
|
+
);
|
|
291
|
+
if (!clientId || !clientSecret)
|
|
292
|
+
throw usageError("--app requires --client-id and --client-secret ('-' reads stdin).");
|
|
293
|
+
if (opts.readOnly && opts.scope !== undefined)
|
|
294
|
+
throw usageError("--read-only cannot be combined with --scope.");
|
|
295
|
+
const scopes = clientCredentialsScopes(
|
|
296
|
+
opts.readOnly ? "read" : (opts.scope ?? process.env.LINEAR_CLIENT_SCOPES),
|
|
297
|
+
);
|
|
298
|
+
const provider = new ClientCredentialsTokenProvider({ clientId, clientSecret, scopes });
|
|
299
|
+
const token = await provider.getAccessToken();
|
|
300
|
+
const identity = await validateAuthCredential(
|
|
301
|
+
token.accessToken,
|
|
302
|
+
ctx.options.workspace,
|
|
303
|
+
"oauth-access-token",
|
|
304
|
+
);
|
|
305
|
+
const saved = writeAppCredential(
|
|
306
|
+
{
|
|
307
|
+
version: 1,
|
|
308
|
+
kind: "oauth-app",
|
|
309
|
+
clientId,
|
|
310
|
+
clientSecret,
|
|
311
|
+
scopes: token.scope.split(/[ ,]+/).filter(Boolean),
|
|
312
|
+
workspace: identity.organization,
|
|
313
|
+
},
|
|
314
|
+
{ plaintext: opts.plaintext === true },
|
|
315
|
+
);
|
|
316
|
+
const projectConfigPath = saveLoginProject(
|
|
317
|
+
ctx,
|
|
318
|
+
identity.organization.urlKey,
|
|
319
|
+
opts.project !== false,
|
|
320
|
+
);
|
|
321
|
+
ctx.output.emit(
|
|
322
|
+
{
|
|
323
|
+
success: true,
|
|
324
|
+
credentialType: "oauth-app",
|
|
325
|
+
workspace: identity.organization.urlKey,
|
|
326
|
+
user: identity.user,
|
|
327
|
+
storage: saved.storage,
|
|
328
|
+
path: saved.path,
|
|
329
|
+
projectConfigPath,
|
|
330
|
+
},
|
|
331
|
+
() =>
|
|
332
|
+
ctx.output.success(
|
|
333
|
+
`Authenticated as an app for workspace '${identity.organization.urlKey}'. ${saved.storage === "file" ? `Client credentials saved to ${saved.path} (plaintext, 0600).` : `Client credentials saved to the ${saved.keyringLabel}.`}`,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (opts.clientSecret !== undefined || opts.scope !== undefined)
|
|
339
|
+
throw usageError("--client-secret and --scope require --app or --client-credentials.");
|
|
246
340
|
let key: string | undefined = selectLoginKey(opts.key, ctx.options.apiKey);
|
|
247
341
|
if (key !== undefined) {
|
|
248
342
|
if (
|
|
@@ -291,7 +385,7 @@ export function registerMeta(program: Command): void {
|
|
|
291
385
|
|
|
292
386
|
if (opts.plaintext) {
|
|
293
387
|
throw usageError(
|
|
294
|
-
"OAuth
|
|
388
|
+
"Browser OAuth is keyring-only; --plaintext requires --key, --api-key, or --client-credentials.",
|
|
295
389
|
);
|
|
296
390
|
}
|
|
297
391
|
if (opts.readOnly && opts.admin) {
|
|
@@ -462,7 +556,7 @@ export function registerMeta(program: Command): void {
|
|
|
462
556
|
.action(
|
|
463
557
|
action(async (ctx: Context) => {
|
|
464
558
|
const c = ctx.config;
|
|
465
|
-
if (c.accessToken) {
|
|
559
|
+
if (c.accessToken || c.appCredential) {
|
|
466
560
|
throw usageError(
|
|
467
561
|
"`auth token` exports stored API keys only. OAuth access tokens are never exported; use the authenticated command directly.",
|
|
468
562
|
);
|
|
@@ -490,19 +584,21 @@ export function registerMeta(program: Command): void {
|
|
|
490
584
|
const c = ctx.config;
|
|
491
585
|
if (c.workspaceChoices) throw c.apiKeyError;
|
|
492
586
|
const credential = c.accessToken ?? c.apiKey;
|
|
587
|
+
const authenticated = Boolean(credential || c.appCredential);
|
|
588
|
+
const redacted = c.appCredential ? "(client credentials redacted)" : redactKey(credential);
|
|
493
589
|
const credentialType = resolvedCredentialType(c);
|
|
494
590
|
const authorization = authorizationCapabilities(c);
|
|
495
|
-
const source = c.accessToken ? c.accessTokenSource : c.apiKeySource;
|
|
591
|
+
const source = c.accessToken || c.appCredential ? c.accessTokenSource : c.apiKeySource;
|
|
496
592
|
const backend = keyring();
|
|
497
593
|
ctx.output.detail(
|
|
498
594
|
{
|
|
499
|
-
authenticated
|
|
595
|
+
authenticated,
|
|
500
596
|
credentialType,
|
|
501
597
|
source,
|
|
502
598
|
workspace: c.credentialWorkspace ?? null,
|
|
503
|
-
key:
|
|
599
|
+
key: redacted,
|
|
504
600
|
keyring: backend?.name ?? null,
|
|
505
|
-
scopes: c.oauthCredential?.scopes ?? null,
|
|
601
|
+
scopes: (c.oauthCredential ?? c.appCredential)?.scopes ?? null,
|
|
506
602
|
expiresAt: c.oauthCredential
|
|
507
603
|
? new Date(c.oauthCredential.expiresAt).toISOString()
|
|
508
604
|
: null,
|
|
@@ -511,13 +607,13 @@ export function registerMeta(program: Command): void {
|
|
|
511
607
|
scopeNote: authorization.note,
|
|
512
608
|
},
|
|
513
609
|
[
|
|
514
|
-
["Authenticated",
|
|
610
|
+
["Authenticated", authenticated],
|
|
515
611
|
["Credential type", credentialType ?? "(none)"],
|
|
516
612
|
["Source", source],
|
|
517
613
|
["Workspace", c.credentialWorkspace ?? "(none)"],
|
|
518
|
-
["Credential",
|
|
614
|
+
["Credential", redacted],
|
|
519
615
|
["Keyring", backend ? backend.label : "(none on this platform)"],
|
|
520
|
-
["Scopes", c.oauthCredential?.scopes.join(", ")],
|
|
616
|
+
["Scopes", (c.oauthCredential ?? c.appCredential)?.scopes.join(", ")],
|
|
521
617
|
["Scope visibility", authorization.scopeVisibility],
|
|
522
618
|
["Admin scope", authorization.adminScope ?? "unknown"],
|
|
523
619
|
["Scope note", authorization.note],
|
|
@@ -589,9 +685,11 @@ export function registerMeta(program: Command): void {
|
|
|
589
685
|
}
|
|
590
686
|
}
|
|
591
687
|
const local =
|
|
592
|
-
entry?.credentialType === "oauth-
|
|
593
|
-
? removeOAuthCredential(slug)
|
|
594
|
-
:
|
|
688
|
+
entry?.credentialType === "oauth-app"
|
|
689
|
+
? removeOAuthCredential(slug, "app")
|
|
690
|
+
: entry?.credentialType === "oauth-user"
|
|
691
|
+
? removeOAuthCredential(slug)
|
|
692
|
+
: { ...removeCredentialWithMetadata(slug), fallbackCredentialType: null };
|
|
595
693
|
ctx.output.emit(
|
|
596
694
|
{
|
|
597
695
|
success: true,
|
|
@@ -745,7 +843,7 @@ export function registerMeta(program: Command): void {
|
|
|
745
843
|
const profileWorkspace = opts.user && key === "team" ? ctx.options.workspace : undefined;
|
|
746
844
|
if (profileWorkspace) {
|
|
747
845
|
let team = clean;
|
|
748
|
-
if (ctx.config.apiKey || ctx.config.accessToken) {
|
|
846
|
+
if (ctx.config.apiKey || ctx.config.accessToken || ctx.config.appCredential) {
|
|
749
847
|
team = await workspaceTeamValidator(ctx, profileWorkspace, clean);
|
|
750
848
|
}
|
|
751
849
|
path = setWorkspaceTeam(profileWorkspace, team);
|