@mkterswingman/5mghost-twinkler 0.1.4 → 0.1.5
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/dist/auth.d.ts +2 -0
- package/dist/auth.js +119 -18
- package/dist/cli.js +24 -9
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface TokenProviderOptions {
|
|
|
21
21
|
env?: NodeJS.ProcessEnv;
|
|
22
22
|
fetchImpl?: typeof fetch;
|
|
23
23
|
authUrl?: string;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
26
|
}
|
|
25
27
|
export declare function getAuthStatus(options: TokenProviderOptions): AuthStatus;
|
|
26
28
|
export declare function getValidToken(options: TokenProviderOptions): Promise<string | null>;
|
package/dist/auth.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
export const MKTERSWINGMAN_PAT_ENV = "MKTERSWINGMAN_PAT";
|
|
4
4
|
export const PAT_LOGIN_URL = "https://mkterswingman.com/pat/login";
|
|
5
5
|
const DEFAULT_AUTH_URL = "https://mkterswingman.com";
|
|
6
6
|
const REFRESH_SKEW_MS = 60_000;
|
|
7
|
+
const REFRESH_LOCK_STALE_MS = 30_000;
|
|
8
|
+
const REFRESH_LOCK_TIMEOUT_MS = 90_000;
|
|
9
|
+
const REFRESH_LOCK_POLL_MS = 50;
|
|
7
10
|
export function getAuthStatus(options) {
|
|
8
11
|
if (options.env?.[MKTERSWINGMAN_PAT_ENV]) {
|
|
9
12
|
return {
|
|
@@ -54,26 +57,35 @@ export async function getValidToken(options) {
|
|
|
54
57
|
return null;
|
|
55
58
|
if (auth.type === "pat")
|
|
56
59
|
return auth.pat || null;
|
|
57
|
-
if (auth
|
|
60
|
+
if (hasUsableAccessToken(auth, options.now)) {
|
|
58
61
|
return auth.access_token;
|
|
59
62
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
return withRefreshLock(options, async () => {
|
|
64
|
+
const latest = readSharedAuth(options.authJsonPath);
|
|
65
|
+
if (!latest)
|
|
66
|
+
return null;
|
|
67
|
+
if (latest.type === "pat")
|
|
68
|
+
return latest.pat || null;
|
|
69
|
+
if (hasUsableAccessToken(latest, options.now))
|
|
70
|
+
return latest.access_token;
|
|
71
|
+
if (!latest.refresh_token)
|
|
72
|
+
return null;
|
|
73
|
+
const refreshed = await refreshJwt(latest.refresh_token, {
|
|
74
|
+
clientId: latest.client_id,
|
|
75
|
+
authUrl: options.authUrl,
|
|
76
|
+
fetchImpl: options.fetchImpl
|
|
77
|
+
});
|
|
78
|
+
if (!refreshed)
|
|
79
|
+
return null;
|
|
80
|
+
writeSharedAuth(options.authJsonPath, {
|
|
81
|
+
type: "jwt",
|
|
82
|
+
access_token: refreshed.access_token,
|
|
83
|
+
refresh_token: refreshed.refresh_token,
|
|
84
|
+
expires_at: (options.now ?? Date.now)() + refreshed.expires_in * 1000,
|
|
85
|
+
client_id: latest.client_id
|
|
86
|
+
});
|
|
87
|
+
return refreshed.access_token;
|
|
75
88
|
});
|
|
76
|
-
return refreshed.access_token;
|
|
77
89
|
}
|
|
78
90
|
export function savePat(authJsonPath, pat) {
|
|
79
91
|
const normalized = pat.trim();
|
|
@@ -112,6 +124,95 @@ export function writeSharedAuth(authJsonPath, data) {
|
|
|
112
124
|
// Windows may ignore POSIX modes.
|
|
113
125
|
}
|
|
114
126
|
}
|
|
127
|
+
function hasUsableAccessToken(auth, now) {
|
|
128
|
+
return Boolean(auth.access_token &&
|
|
129
|
+
auth.expires_at &&
|
|
130
|
+
(now ?? Date.now)() + REFRESH_SKEW_MS < auth.expires_at);
|
|
131
|
+
}
|
|
132
|
+
async function withRefreshLock(options, fn) {
|
|
133
|
+
const lockPath = `${options.authJsonPath}.refresh.lock`;
|
|
134
|
+
const now = options.now ?? Date.now;
|
|
135
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
136
|
+
const deadline = now() + REFRESH_LOCK_TIMEOUT_MS;
|
|
137
|
+
mkdirSync(dirname(options.authJsonPath), { recursive: true });
|
|
138
|
+
while (true) {
|
|
139
|
+
try {
|
|
140
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
141
|
+
try {
|
|
142
|
+
writeLockOwner(lockPath, now());
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return await fn();
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (!isAlreadyExistsError(error))
|
|
157
|
+
throw error;
|
|
158
|
+
if (canRecoverRefreshLock(lockPath, now())) {
|
|
159
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (now() >= deadline) {
|
|
163
|
+
throw new Error("Timed out waiting for mkterswingman auth refresh lock");
|
|
164
|
+
}
|
|
165
|
+
await sleep(REFRESH_LOCK_POLL_MS);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function isAlreadyExistsError(error) {
|
|
170
|
+
return (typeof error === "object" &&
|
|
171
|
+
error !== null &&
|
|
172
|
+
"code" in error &&
|
|
173
|
+
error.code === "EEXIST");
|
|
174
|
+
}
|
|
175
|
+
function writeLockOwner(lockPath, createdAtMs) {
|
|
176
|
+
writeFileSync(`${lockPath}/owner.json`, JSON.stringify({ pid: process.pid, created_at: createdAtMs }), { encoding: "utf8", mode: 0o600 });
|
|
177
|
+
}
|
|
178
|
+
function canRecoverRefreshLock(lockPath, nowMs) {
|
|
179
|
+
const owner = readRefreshLockOwner(lockPath);
|
|
180
|
+
if (owner) {
|
|
181
|
+
return nowMs - owner.created_at > REFRESH_LOCK_STALE_MS && !isProcessAlive(owner.pid);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return nowMs - statSync(lockPath).mtimeMs > REFRESH_LOCK_STALE_MS;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function readRefreshLockOwner(lockPath) {
|
|
191
|
+
try {
|
|
192
|
+
const owner = JSON.parse(readFileSync(`${lockPath}/owner.json`, "utf8"));
|
|
193
|
+
if (typeof owner.pid === "number" && typeof owner.created_at === "number") {
|
|
194
|
+
return { pid: owner.pid, created_at: owner.created_at };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
function isProcessAlive(pid) {
|
|
203
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
204
|
+
return false;
|
|
205
|
+
try {
|
|
206
|
+
process.kill(pid, 0);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
return (typeof error === "object" &&
|
|
211
|
+
error !== null &&
|
|
212
|
+
"code" in error &&
|
|
213
|
+
error.code === "EPERM");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
115
216
|
async function refreshJwt(refreshToken, options) {
|
|
116
217
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
117
218
|
const response = await fetchImpl(`${options.authUrl ?? DEFAULT_AUTH_URL}/oauth/token`, {
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { getAuthStatus, logout, PAT_LOGIN_URL, savePat } from "./auth.js";
|
|
6
|
+
import { getAuthStatus, getValidToken, logout, PAT_LOGIN_URL, savePat } from "./auth.js";
|
|
7
7
|
import { resolveTwinklerPaths } from "./paths.js";
|
|
8
8
|
import { installBundledSkills } from "./skillInstall.js";
|
|
9
9
|
import { callTwinkler } from "./twinklerClient.js";
|
|
@@ -51,20 +51,28 @@ export async function runCli(context = {}) {
|
|
|
51
51
|
out(readPackageVersion());
|
|
52
52
|
return 0;
|
|
53
53
|
case "auth":
|
|
54
|
-
return await runAuthCommand(args, {
|
|
54
|
+
return await runAuthCommand(args, {
|
|
55
|
+
paths,
|
|
56
|
+
env,
|
|
57
|
+
out,
|
|
58
|
+
stdin: context.stdin ?? process.stdin,
|
|
59
|
+
fetchImpl: context.fetchImpl
|
|
60
|
+
});
|
|
55
61
|
case "login":
|
|
56
62
|
return await runAuthCommand(["login", "--pat-stdin", ...args.filter((arg) => arg !== "--pat-stdin")], {
|
|
57
63
|
paths,
|
|
58
64
|
env,
|
|
59
65
|
out,
|
|
60
|
-
stdin: context.stdin ?? process.stdin
|
|
66
|
+
stdin: context.stdin ?? process.stdin,
|
|
67
|
+
fetchImpl: context.fetchImpl
|
|
61
68
|
});
|
|
62
69
|
case "ensure":
|
|
63
|
-
return runEnsureCommand(args, {
|
|
70
|
+
return await runEnsureCommand(args, {
|
|
64
71
|
paths,
|
|
65
72
|
env,
|
|
66
73
|
out,
|
|
67
74
|
err,
|
|
75
|
+
fetchImpl: context.fetchImpl,
|
|
68
76
|
spawnSyncImpl: context.spawnSyncImpl ?? spawnSync
|
|
69
77
|
});
|
|
70
78
|
case "call": {
|
|
@@ -92,7 +100,7 @@ export async function runCli(context = {}) {
|
|
|
92
100
|
}
|
|
93
101
|
case "setup": {
|
|
94
102
|
const results = installBundledSkills({ homeDir: paths.homeDir });
|
|
95
|
-
const auth =
|
|
103
|
+
const auth = await getVerifiedAuthStatus(paths, env, context.fetchImpl);
|
|
96
104
|
out([
|
|
97
105
|
"Twinkler setup",
|
|
98
106
|
renderSkillInstallResults(results),
|
|
@@ -106,7 +114,7 @@ export async function runCli(context = {}) {
|
|
|
106
114
|
case "upgrade":
|
|
107
115
|
return runUpdateCommand(args, { env, out, err, spawnSyncImpl: context.spawnSyncImpl ?? spawnSync });
|
|
108
116
|
case "doctor": {
|
|
109
|
-
const auth =
|
|
117
|
+
const auth = await getVerifiedAuthStatus(paths, env, context.fetchImpl);
|
|
110
118
|
out([
|
|
111
119
|
"Twinkler doctor",
|
|
112
120
|
`Node: ${process.version}`,
|
|
@@ -145,7 +153,7 @@ async function runAuthCommand(args, context) {
|
|
|
145
153
|
return 0;
|
|
146
154
|
}
|
|
147
155
|
if (subcommand === "status") {
|
|
148
|
-
const status =
|
|
156
|
+
const status = await getVerifiedAuthStatus(context.paths, context.env, context.fetchImpl);
|
|
149
157
|
if (!status.authenticated) {
|
|
150
158
|
context.out([
|
|
151
159
|
"Not logged in.",
|
|
@@ -184,12 +192,12 @@ async function runAuthCommand(args, context) {
|
|
|
184
192
|
context.out(`Unknown auth subcommand: ${subcommand}`);
|
|
185
193
|
return 1;
|
|
186
194
|
}
|
|
187
|
-
function runEnsureCommand(args, context) {
|
|
195
|
+
async function runEnsureCommand(args, context) {
|
|
188
196
|
const json = args.includes("--json");
|
|
189
197
|
const autoUpdate = args.includes("--auto-update");
|
|
190
198
|
const localVersion = readPackageVersion();
|
|
191
199
|
const latest = readNpmLatestVersion(context.spawnSyncImpl, context.env);
|
|
192
|
-
const auth =
|
|
200
|
+
const auth = await getVerifiedAuthStatus(context.paths, context.env, context.fetchImpl);
|
|
193
201
|
const updateNeeded = latest.version !== null && compareSemver(latest.version, localVersion) > 0;
|
|
194
202
|
let update = {
|
|
195
203
|
needed: updateNeeded,
|
|
@@ -242,6 +250,13 @@ function runEnsureCommand(args, context) {
|
|
|
242
250
|
context.err(update.error);
|
|
243
251
|
return exitCode;
|
|
244
252
|
}
|
|
253
|
+
async function getVerifiedAuthStatus(paths, env, fetchImpl) {
|
|
254
|
+
const auth = getAuthStatus({ authJsonPath: paths.authJsonPath, env });
|
|
255
|
+
if (!auth.authenticated)
|
|
256
|
+
return auth;
|
|
257
|
+
const token = await getValidToken({ authJsonPath: paths.authJsonPath, env, fetchImpl });
|
|
258
|
+
return { ...auth, authenticated: Boolean(token) };
|
|
259
|
+
}
|
|
245
260
|
function renderEnsureResult(result) {
|
|
246
261
|
const latest = result.latest_version ?? `unknown (${result.latest_check.ok ? "not checked" : result.latest_check.error})`;
|
|
247
262
|
const lines = [
|