@danypops/tickets 0.9.0 → 0.10.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/package.json +1 -1
- package/src/auth/browser.ts +8 -1
- package/src/auth/token-store.ts +13 -10
- package/src/cli/index.ts +17 -15
- package/src/cli/tickets-client.ts +10 -2
- package/src/config/config.ts +1 -1
package/package.json
CHANGED
package/src/auth/browser.ts
CHANGED
|
@@ -9,8 +9,15 @@ import { spawn } from "node:child_process";
|
|
|
9
9
|
|
|
10
10
|
export type Spawner = (command: string, args: string[]) => void;
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// A spawn() failure (e.g. no `xdg-open` on a minimal Linux install) surfaces asynchronously as
|
|
13
|
+
// an unlistened "error" event under Node, which is an uncaught exception that kills the whole
|
|
14
|
+
// host process -- this runs from inside the long-lived pi-tickets extension host (tui.ts), not
|
|
15
|
+
// just the standalone CLI, so that crash is a real live risk, not just a CLI inconvenience.
|
|
16
|
+
export const defaultSpawner: Spawner = (command, args) => {
|
|
13
17
|
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
18
|
+
child.on("error", (error) => {
|
|
19
|
+
console.error(`failed to open URL via ${command}: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
});
|
|
14
21
|
child.unref();
|
|
15
22
|
};
|
|
16
23
|
|
package/src/auth/token-store.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* *to* GitHub/GitLab/Jira on the user's behalf. Same security posture as
|
|
6
6
|
* vehicle-server's ensureAuthToken: 0700 directory, 0600 files, atomic write.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
9
|
+
import { mkdir } from "node:fs/promises";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
12
|
+
import { createAtomicJsonWriter } from "@danypops/vehicle-core";
|
|
13
|
+
import { createNodeAtomicJsonFsAdapter } from "@danypops/vehicle-server/atomic-json";
|
|
14
|
+
|
|
15
|
+
const atomicJson = createAtomicJsonWriter({ fs: createNodeAtomicJsonFsAdapter() });
|
|
11
16
|
|
|
12
17
|
export interface StoredToken {
|
|
13
18
|
accessToken: string;
|
|
@@ -35,21 +40,19 @@ function tokenPath(backend: string, opts: TokenStoreEnv = {}): string {
|
|
|
35
40
|
return join(tokenStoreDir(opts), `${backend}.json`);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
export function saveToken(backend: string, token: StoredToken, opts: TokenStoreEnv = {}): void {
|
|
43
|
+
export async function saveToken(backend: string, token: StoredToken, opts: TokenStoreEnv = {}): Promise<void> {
|
|
39
44
|
const path = tokenPath(backend, opts);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
writeFileSync(temp, `${JSON.stringify(token, null, 2)}\n`, { mode: 0o600 });
|
|
43
|
-
renameSync(temp, path);
|
|
45
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
46
|
+
await atomicJson.write(path, token, { mode: 0o600, pretty: true, trailingNewline: true });
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export function loadToken(backend: string, opts: TokenStoreEnv = {}): StoredToken | undefined {
|
|
49
|
+
export async function loadToken(backend: string, opts: TokenStoreEnv = {}): Promise<StoredToken | undefined> {
|
|
47
50
|
const path = tokenPath(backend, opts);
|
|
48
|
-
if (!existsSync(path)) return undefined;
|
|
49
|
-
chmodSync(path, 0o600);
|
|
50
51
|
try {
|
|
51
|
-
return
|
|
52
|
+
return (await atomicJson.read(path)) as StoredToken | undefined;
|
|
52
53
|
} catch {
|
|
54
|
+
// A corrupt/malformed token file is treated the same as no token --
|
|
55
|
+
// the caller re-authenticates rather than crashing on a parse error.
|
|
53
56
|
return undefined;
|
|
54
57
|
}
|
|
55
58
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -550,7 +550,7 @@ auth
|
|
|
550
550
|
if (type === "github" && opts.ghCli !== undefined) {
|
|
551
551
|
const result = await readGhCliToken(opts.ghCli === true ? undefined : opts.ghCli);
|
|
552
552
|
if (!result.ok) throw new Error(result.reason);
|
|
553
|
-
saveToken(opts.backend, { accessToken: result.token });
|
|
553
|
+
await saveToken(opts.backend, { accessToken: result.token });
|
|
554
554
|
printJson({
|
|
555
555
|
backend: opts.backend,
|
|
556
556
|
status: "authorized",
|
|
@@ -577,7 +577,7 @@ auth
|
|
|
577
577
|
}
|
|
578
578
|
},
|
|
579
579
|
});
|
|
580
|
-
saveToken(opts.backend, {
|
|
580
|
+
await saveToken(opts.backend, {
|
|
581
581
|
accessToken: token.accessToken,
|
|
582
582
|
refreshToken: token.refreshToken,
|
|
583
583
|
expiresAt: token.expiresAt,
|
|
@@ -600,7 +600,7 @@ auth
|
|
|
600
600
|
}
|
|
601
601
|
},
|
|
602
602
|
});
|
|
603
|
-
saveToken(opts.backend, {
|
|
603
|
+
await saveToken(opts.backend, {
|
|
604
604
|
accessToken: token.accessToken,
|
|
605
605
|
refreshToken: token.refreshToken,
|
|
606
606
|
expiresAt: token.expiresAt,
|
|
@@ -627,7 +627,7 @@ auth
|
|
|
627
627
|
}
|
|
628
628
|
},
|
|
629
629
|
});
|
|
630
|
-
saveToken(opts.backend, {
|
|
630
|
+
await saveToken(opts.backend, {
|
|
631
631
|
accessToken: token.accessToken,
|
|
632
632
|
refreshToken: token.refreshToken,
|
|
633
633
|
expiresAt: token.expiresAt,
|
|
@@ -661,7 +661,7 @@ auth
|
|
|
661
661
|
process.exitCode = 1;
|
|
662
662
|
return;
|
|
663
663
|
}
|
|
664
|
-
saveToken(backend, { accessToken: value });
|
|
664
|
+
await saveToken(backend, { accessToken: value });
|
|
665
665
|
printJson({
|
|
666
666
|
backend,
|
|
667
667
|
status: "stored",
|
|
@@ -672,18 +672,20 @@ auth
|
|
|
672
672
|
auth
|
|
673
673
|
.command("status")
|
|
674
674
|
.description("list backends with a locally stored delegated token")
|
|
675
|
-
.action(() => {
|
|
675
|
+
.action(async () => {
|
|
676
676
|
const backends = listStoredBackends();
|
|
677
677
|
printJson(
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
678
|
+
await Promise.all(
|
|
679
|
+
backends.map(async (backend) => {
|
|
680
|
+
const token = await loadToken(backend);
|
|
681
|
+
return {
|
|
682
|
+
backend,
|
|
683
|
+
fresh: token ? isTokenFresh(token) : false,
|
|
684
|
+
expiresAt: token?.expiresAt ?? null,
|
|
685
|
+
scope: token?.scope ?? null,
|
|
686
|
+
};
|
|
687
|
+
}),
|
|
688
|
+
),
|
|
687
689
|
);
|
|
688
690
|
});
|
|
689
691
|
|
|
@@ -29,14 +29,22 @@ async function isAlive(handle: DaemonHandle, token: string): Promise<boolean> {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Absolute path to the daemon's real entry point, resolved from this package's own root. Used
|
|
32
|
+
/** Absolute path to the daemon's real entry point, resolved from this package's own root. Used to spawn it on demand -- Armada's own ServiceSpec (see cli/systemd-service.ts) launches the CLI's own `serve` command instead, not this path directly. */
|
|
33
33
|
export function resolveDaemonEntryPath(): string {
|
|
34
34
|
const root = packageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
35
35
|
return join(root, "src", "process", "main.ts");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// A spawn() failure surfaces asynchronously as an unlistened "error" event under Node, which is
|
|
39
|
+
// an uncaught exception that kills the whole host process. createTicketsClient (which calls
|
|
40
|
+
// this via ensureDaemonRunning) runs from inside the long-lived pi-tickets extension host
|
|
41
|
+
// (tui.ts) as well as the CLI, so a missing/misconfigured `bun` binary would otherwise crash
|
|
42
|
+
// the whole Pi session, not just this one connect attempt.
|
|
43
|
+
export function spawnDaemon(): void {
|
|
39
44
|
const child = spawn("bun", ["run", resolveDaemonEntryPath()], { detached: true, stdio: "ignore" });
|
|
45
|
+
child.on("error", (error) => {
|
|
46
|
+
console.error(`tickets daemon auto-spawn failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
});
|
|
40
48
|
child.unref();
|
|
41
49
|
}
|
|
42
50
|
|
package/src/config/config.ts
CHANGED
|
@@ -103,7 +103,7 @@ export async function preferredAuth(
|
|
|
103
103
|
const fromEnigma = await tryEnigma(name, { env, token: env.ENIGMA_CLIENT_TOKEN });
|
|
104
104
|
if (fromEnigma) return { token: fromEnigma.accessToken, oauth: true, extra: fromEnigma.extra };
|
|
105
105
|
|
|
106
|
-
const stored = loadToken(name, { env });
|
|
106
|
+
const stored = await loadToken(name, { env });
|
|
107
107
|
if (stored && isTokenFresh(stored)) {
|
|
108
108
|
return { token: stored.accessToken, oauth: true, extra: stored.extra };
|
|
109
109
|
}
|