@danypops/tickets 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/package.json +1 -1
- package/src/adapters/jira.ts +4 -1
- package/src/auth/masked-prompt.ts +54 -0
- package/src/cli/index.ts +18 -0
package/README.md
CHANGED
|
@@ -171,6 +171,22 @@ tickets auth status
|
|
|
171
171
|
tickets auth logout github
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
### Static token instead (API key/PAT, no OAuth)
|
|
175
|
+
|
|
176
|
+
For a backend with no OAuth app to register against (e.g. a plain Atlassian
|
|
177
|
+
API token from `id.atlassian.com/manage-profile/security/api-tokens`),
|
|
178
|
+
store it directly -- same 0600 local file `auth login` writes to, checked
|
|
179
|
+
ahead of any `token`/`token_env` config-file or plain env-var fallback:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
tickets auth set-token jira
|
|
183
|
+
# Paste the "jira" token (input hidden): ****
|
|
184
|
+
|
|
185
|
+
# or non-interactively, e.g. from a password manager (piped stdin works too,
|
|
186
|
+
# since a non-TTY stdin is read as-is with nothing to mask):
|
|
187
|
+
pass show jira-api-token | tickets auth set-token jira
|
|
188
|
+
```
|
|
189
|
+
|
|
174
190
|
### GitHub: reuse an already-authenticated `gh` CLI session
|
|
175
191
|
|
|
176
192
|
`tickets auth login --backend github --gh-cli [account]` skips the device
|
package/package.json
CHANGED
package/src/adapters/jira.ts
CHANGED
|
@@ -264,8 +264,11 @@ export class JiraRepository {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
private async searchJql(jql: string, limit: number): Promise<Issue[]> {
|
|
267
|
+
// searchForIssuesUsingJqlPost hits the deprecated /rest/api/2/search, which
|
|
268
|
+
// Atlassian has sunset on Jira Cloud (410 Gone). The enhanced variant posts
|
|
269
|
+
// to the still-live /rest/api/2/search/jql with an identical request/response shape.
|
|
267
270
|
const result = await this.call<{ issues?: JiraIssue[] }>(() =>
|
|
268
|
-
this.client.issueSearch.
|
|
271
|
+
this.client.issueSearch.searchForIssuesUsingJqlEnhancedSearchPost({ jql, maxResults: limit }),
|
|
269
272
|
);
|
|
270
273
|
return (result?.issues ?? []).map((raw) => this.toDomain(raw));
|
|
271
274
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a secret from a real TTY with keystrokes masked -- never echoed to
|
|
3
|
+
* the terminal, so it can't land in scrollback or a screen share the way a
|
|
4
|
+
* typed-and-visible value would. Falls back to reading one line from stdin
|
|
5
|
+
* unmasked when stdin isn't a TTY (piped input, e.g. `pass show jira |
|
|
6
|
+
* tickets auth set-token jira`) -- there is nothing to mask once the value
|
|
7
|
+
* never touched an interactive terminal in the first place.
|
|
8
|
+
*/
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
|
|
11
|
+
export function promptMaskedSecret(
|
|
12
|
+
promptText: string,
|
|
13
|
+
input: NodeJS.ReadableStream = process.stdin,
|
|
14
|
+
output: NodeJS.WritableStream = process.stdout,
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const isTTY = (input as NodeJS.ReadStream).isTTY === true;
|
|
17
|
+
|
|
18
|
+
if (!isTTY) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let data = "";
|
|
21
|
+
input.setEncoding?.("utf8");
|
|
22
|
+
input.on("data", (chunk) => {
|
|
23
|
+
data += chunk;
|
|
24
|
+
});
|
|
25
|
+
input.on("end", () => resolve(data.split("\n")[0]?.trim() ?? ""));
|
|
26
|
+
input.on("error", reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const rl = createInterface({ input, output, terminal: true });
|
|
32
|
+
// readline has no public masking option; overriding the internal
|
|
33
|
+
// _writeToOutput hook (long-standing, widely-used pattern for exactly
|
|
34
|
+
// this) is the only way to suppress echoed keystrokes while still
|
|
35
|
+
// letting the prompt text itself render once.
|
|
36
|
+
// biome-ignore lint/suspicious/noExplicitAny: readline's internal _writeToOutput has no public type
|
|
37
|
+
const rlInternal = rl as any;
|
|
38
|
+
const originalWriteToOutput = rlInternal._writeToOutput.bind(rl);
|
|
39
|
+
let promptShown = false;
|
|
40
|
+
rlInternal._writeToOutput = (stringToWrite: string) => {
|
|
41
|
+
if (!promptShown) {
|
|
42
|
+
originalWriteToOutput(stringToWrite);
|
|
43
|
+
if (stringToWrite.includes(promptText)) promptShown = true;
|
|
44
|
+
}
|
|
45
|
+
// Every keystroke after the prompt itself is swallowed -- masked.
|
|
46
|
+
};
|
|
47
|
+
rl.question(promptText, (answer) => {
|
|
48
|
+
rl.close();
|
|
49
|
+
output.write("\n");
|
|
50
|
+
resolve(answer.trim());
|
|
51
|
+
});
|
|
52
|
+
rl.on("error", reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { readGhCliToken } from "../auth/gh-cli.js";
|
|
|
15
15
|
import { gitlabDeviceEndpoints, loginWithGitLabDeviceFlow } from "../auth/gitlab-oauth.js";
|
|
16
16
|
import { loginWithJiraAuthorizationCode } from "../auth/jira-oauth.js";
|
|
17
17
|
import { deleteToken, isTokenFresh, listStoredBackends, loadToken, saveToken } from "../auth/token-store.js";
|
|
18
|
+
import { promptMaskedSecret } from "../auth/masked-prompt.js";
|
|
18
19
|
import { installTicketsService, systemctlTickets, systemdUnitPath } from "./systemd-service.js";
|
|
19
20
|
|
|
20
21
|
function printJson(value: unknown): void {
|
|
@@ -422,6 +423,23 @@ auth
|
|
|
422
423
|
}
|
|
423
424
|
});
|
|
424
425
|
|
|
426
|
+
auth
|
|
427
|
+
.command("set-token <backend>")
|
|
428
|
+
.description("store a plain static token (API key/PAT, no OAuth) for a backend -- e.g. an Atlassian API token for jira")
|
|
429
|
+
.action(async (backend: string) => {
|
|
430
|
+
// TICKETS_TOKEN_VALUE remains for non-interactive/scripted use (a provisioning
|
|
431
|
+
// script, `pass show jira | tickets auth set-token jira`) -- never accepted as a
|
|
432
|
+
// plain CLI argument, which would land in shell history the way this would not.
|
|
433
|
+
const value = process.env.TICKETS_TOKEN_VALUE ?? (await promptMaskedSecret(`Paste the "${backend}" token (input hidden): `));
|
|
434
|
+
if (!value) {
|
|
435
|
+
process.stderr.write("no token value provided — paste one at the prompt, or set TICKETS_TOKEN_VALUE for non-interactive use\n");
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
saveToken(backend, { accessToken: value });
|
|
440
|
+
printJson({ backend, status: "stored", note: "restart the tickets daemon (or run `tickets daemon-status` after a fresh start) to pick up the new token" });
|
|
441
|
+
});
|
|
442
|
+
|
|
425
443
|
auth
|
|
426
444
|
.command("status")
|
|
427
445
|
.description("list backends with a locally stored delegated token")
|