@moewolf/moe-icons-cli 0.0.8 → 0.0.10
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
CHANGED
|
@@ -281,6 +281,12 @@ Log out before switching storage methods. Official production refresh uses the
|
|
|
281
281
|
existing public Auth0 configuration; custom/local environments must provide
|
|
282
282
|
matching issuer and client ID. Account and refresh requests have a deadline
|
|
283
283
|
covering headers and body. Revoked refresh credentials require a new login.
|
|
284
|
+
Overlapping CLI processes coordinate refreshes and reuse the same renewed
|
|
285
|
+
session. A cancelled waiter does not cancel the refresh owned by another process.
|
|
286
|
+
After a process is forcibly stopped during refresh, a remaining
|
|
287
|
+
`session-refresh.lock` in the selected store directory (or the CLI state directory
|
|
288
|
+
for native credentials) reports an error. Confirm the PID in its `owner.json`
|
|
289
|
+
has stopped before removing that lock directory and retrying.
|
|
284
290
|
|
|
285
291
|
The VS Code extension reads credentials on its own host. For Remote SSH, WSL
|
|
286
292
|
or a container, run the CLI login on that host; local credentials are not copied.
|
|
@@ -72,34 +72,53 @@ export async function loginWithDeviceSession(config, deps, signal) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
export async function refreshAuth0Session(config, session, deps) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
75
|
+
const perform = async () => {
|
|
76
|
+
const current = deps.tokenStore.getActive();
|
|
77
|
+
if (!current || current.accountId !== session.accountId)
|
|
78
|
+
throw new CliError("AUTH_ERROR", "session changed during refresh; retry with the current account");
|
|
79
|
+
if (current.accessToken !== session.accessToken || current.storedAt !== session.storedAt) {
|
|
80
|
+
if (current.expiresAt > deps.now())
|
|
81
|
+
return current;
|
|
82
|
+
throw new CliError("NETWORK_ERROR", "session changed during refresh; retry with the current account");
|
|
83
|
+
}
|
|
84
|
+
const response = await boundedResponse(deps.fetch, `${config.auth0Issuer.replace(/\/$/, "")}/oauth/token`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "content-type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
grant_type: "refresh_token",
|
|
89
|
+
client_id: config.auth0ClientId,
|
|
90
|
+
refresh_token: session.refreshToken,
|
|
91
|
+
}),
|
|
92
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
93
|
+
});
|
|
94
|
+
if (response.status >= 500 || response.status === 429)
|
|
95
|
+
throw new CliError("NETWORK_ERROR", "Auth0 refresh temporarily unavailable; retry");
|
|
96
|
+
const body = (await response.json());
|
|
97
|
+
if (!response.ok || typeof body.access_token !== "string") {
|
|
98
|
+
throw new CliError("AUTH_ERROR", "Auth0 refresh failed");
|
|
99
|
+
}
|
|
100
|
+
if (body.expires_in !== undefined &&
|
|
101
|
+
(typeof body.expires_in !== "number" ||
|
|
102
|
+
!Number.isSafeInteger(body.expires_in) ||
|
|
103
|
+
body.expires_in <= 0))
|
|
104
|
+
throw new CliError("VALIDATION_ERROR", "invalid Auth0 token lifetime");
|
|
105
|
+
const updated = {
|
|
106
|
+
...session,
|
|
107
|
+
accessToken: body.access_token,
|
|
108
|
+
refreshToken: typeof body.refresh_token === "string" ? body.refresh_token : session.refreshToken,
|
|
109
|
+
expiresAt: deps.now() + (typeof body.expires_in === "number" ? body.expires_in : 3600) * 1000,
|
|
110
|
+
storedAt: deps.now(),
|
|
111
|
+
};
|
|
112
|
+
const latest = deps.tokenStore.getActive();
|
|
113
|
+
if (!latest ||
|
|
114
|
+
latest.accountId !== session.accountId ||
|
|
115
|
+
latest.accessToken !== session.accessToken ||
|
|
116
|
+
latest.storedAt !== session.storedAt)
|
|
117
|
+
throw new CliError("AUTH_ERROR", "session changed during refresh; retry with the current account");
|
|
118
|
+
deps.tokenStore.set(updated);
|
|
119
|
+
return updated;
|
|
99
120
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
deps.tokenStore.set(updated);
|
|
104
|
-
return updated;
|
|
121
|
+
return deps.tokenStore.withRefreshLock
|
|
122
|
+
? deps.tokenStore.withRefreshLock(perform, deps.signal)
|
|
123
|
+
: perform();
|
|
105
124
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, readFileSync, lstatSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { CliError } from "../errors/index.js";
|
|
5
|
+
/** Coordinates refresh rotation across CLI processes without storing credentials. */
|
|
6
|
+
export async function withRefreshLock(root, operation, signal) {
|
|
7
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
8
|
+
const path = join(root, "session-refresh.lock");
|
|
9
|
+
const deadline = Date.now() + 20_000;
|
|
10
|
+
for (;;) {
|
|
11
|
+
if (signal?.aborted)
|
|
12
|
+
throw new CliError("CANCELLED", "session refresh cancelled");
|
|
13
|
+
try {
|
|
14
|
+
mkdirSync(path, { mode: 0o700 });
|
|
15
|
+
try {
|
|
16
|
+
writeFileSync(join(path, "owner.json"), JSON.stringify({ pid: process.pid }), {
|
|
17
|
+
flag: "wx",
|
|
18
|
+
mode: 0o600,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
rmSync(path, { recursive: true, force: true });
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== "EEXIST")
|
|
29
|
+
throw new CliError("NETWORK_ERROR", "session refresh coordination unavailable; retry");
|
|
30
|
+
try {
|
|
31
|
+
const metadata = lstatSync(path);
|
|
32
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink())
|
|
33
|
+
throw new CliError("NETWORK_ERROR", "invalid session refresh lock; repair credential storage");
|
|
34
|
+
const owner = JSON.parse(readFileSync(join(path, "owner.json"), "utf8"));
|
|
35
|
+
if (Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
36
|
+
try {
|
|
37
|
+
process.kill(owner.pid, 0);
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (cause.code === "ESRCH") {
|
|
41
|
+
throw new CliError("NETWORK_ERROR", "a stopped process left a session refresh lock; repair credential storage before retrying");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
if (cause instanceof CliError)
|
|
48
|
+
throw cause; /* Owner may still be writing its marker. */
|
|
49
|
+
}
|
|
50
|
+
if (Date.now() >= deadline)
|
|
51
|
+
throw new CliError("NETWORK_ERROR", "session refresh is busy; retry or repair a stale credential lock");
|
|
52
|
+
try {
|
|
53
|
+
await delay(50, undefined, signal ? { signal } : undefined);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new CliError("CANCELLED", "session refresh cancelled");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return await operation();
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
rmSync(path, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Canonical CLI/extension session-store selection. Contains no credentials.
|
|
2
2
|
const {homedir} = require('node:os');
|
|
3
3
|
const {join, isAbsolute} = require('node:path');
|
|
4
|
-
const {
|
|
4
|
+
const {lstatSync, readFileSync, mkdirSync, writeFileSync, renameSync} = require('node:fs');
|
|
5
5
|
function stateRoot(env) { const root=env.MOEICONS_STATE_DIR || join(homedir(), '.moeicons'); if(!isAbsolute(root)) throw new Error('MOEICONS_STATE_DIR must be absolute'); return root; }
|
|
6
6
|
function selectedStore(env) {
|
|
7
7
|
if (env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === '1' && env.MOEICONS_TOKEN_STORE_DIR) { if(!isAbsolute(env.MOEICONS_TOKEN_STORE_DIR)) throw new Error('MOEICONS_TOKEN_STORE_DIR must be absolute'); return {mode:'file', rootDir:env.MOEICONS_TOKEN_STORE_DIR}; }
|
|
8
8
|
const path = join(stateRoot(env), 'session-store.json');
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let meta;
|
|
10
|
+
try { meta=lstatSync(path); } catch(error) { if(error.code==='ENOENT') return {mode:env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === '1' ? 'none' : 'system'}; throw error; }
|
|
11
11
|
if (!meta.isFile() || meta.isSymbolicLink() || meta.size>4096 || (process.platform!=='win32' && ((meta.mode & 0o077)!==0 || meta.uid!==process.getuid()))) throw new Error('Session storage preference is not readable securely');
|
|
12
12
|
const value=JSON.parse(readFileSync(path,'utf8'));
|
|
13
13
|
if (value.schemaVersion!==1 || value.mode!=='file' || typeof value.rootDir!=='string' || !isAbsolute(value.rootDir)) throw new Error('Invalid session storage preference');
|
|
@@ -17,6 +17,7 @@ export interface TokenStore {
|
|
|
17
17
|
set(session: StoredSession): void;
|
|
18
18
|
delete(accountId: string): void;
|
|
19
19
|
clear(): void;
|
|
20
|
+
withRefreshLock?<T>(operation: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* File-backed fallback store (mode 0600). Used only when no OS keychain is
|
package/dist/auth/token-store.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, lstatSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { missingCredential } from "./session-policy.cjs";
|
|
4
|
+
import { missingCredential, stateRoot } from "./session-policy.cjs";
|
|
5
|
+
import { withRefreshLock } from "./refresh-lock.js";
|
|
5
6
|
import { CliError } from "../errors/index.js";
|
|
6
7
|
import { execFileSync } from "node:child_process";
|
|
7
8
|
function parseSession(value) {
|
|
@@ -24,10 +25,16 @@ export function createFileTokenStore(options = {}) {
|
|
|
24
25
|
const root = options.rootDir ?? join(homedir(), ".moeicons");
|
|
25
26
|
const file = join(root, "token-store.json");
|
|
26
27
|
function readAll() {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
let meta;
|
|
29
|
+
try {
|
|
30
|
+
meta = lstatSync(file);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === "ENOENT")
|
|
34
|
+
return {};
|
|
35
|
+
throw new CliError("AUTH_ERROR", "file credential storage unavailable; check permissions or repair the store");
|
|
36
|
+
}
|
|
29
37
|
try {
|
|
30
|
-
const meta = lstatSync(file);
|
|
31
38
|
if (!meta.isFile() || meta.isSymbolicLink() || meta.size > 1_000_000 || (process.platform !== "win32" && ((meta.mode & 0o077) !== 0 || typeof process.getuid === "function" && meta.uid !== process.getuid())))
|
|
32
39
|
throw new Error("insecure store");
|
|
33
40
|
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -48,6 +55,7 @@ export function createFileTokenStore(options = {}) {
|
|
|
48
55
|
chmodSync(file, 0o600);
|
|
49
56
|
}
|
|
50
57
|
return {
|
|
58
|
+
withRefreshLock: (operation, signal) => withRefreshLock(root, operation, signal),
|
|
51
59
|
get(accountId) {
|
|
52
60
|
return readAll()[accountId];
|
|
53
61
|
},
|
|
@@ -64,7 +72,7 @@ export function createFileTokenStore(options = {}) {
|
|
|
64
72
|
delete all[accountId];
|
|
65
73
|
writeAll(all);
|
|
66
74
|
},
|
|
67
|
-
clear() { writeAll({}); },
|
|
75
|
+
clear() { readAll(); writeAll({}); },
|
|
68
76
|
};
|
|
69
77
|
}
|
|
70
78
|
/** Use the native credential store when the platform provides a supported CLI. */
|
|
@@ -97,7 +105,7 @@ export function createSystemTokenStore(options = {}) {
|
|
|
97
105
|
const prefix = "[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime];$v=[Windows.Security.Credentials.PasswordVault]::new();";
|
|
98
106
|
read = () => invoke("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `${prefix}try{$c=$v.Retrieve('${service}','${account}');$c.RetrievePassword();[Console]::Out.Write($c.Password)}catch{$e=$_.Exception;while($e){if($e.HResult -eq -2147023728){exit 44};$e=$e.InnerException};exit 45}`]);
|
|
99
107
|
write = (value) => { invoke("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `${prefix}$p=[Console]::In.ReadToEnd();try{$v.Remove($v.Retrieve('${service}','${account}'))}catch{};$v.Add([Windows.Security.Credentials.PasswordCredential]::new('${service}','${account}',$p))`], value); };
|
|
100
|
-
remove = () => { invoke("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `${prefix}try{$v.Remove($v.Retrieve('${service}','${account}'))}catch{}`]); };
|
|
108
|
+
remove = () => { invoke("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `${prefix}try{$v.Remove($v.Retrieve('${service}','${account}'))}catch{$e=$_.Exception;while($e){if($e.HResult -eq -2147023728){exit 44};$e=$e.InnerException};exit 45}`]); };
|
|
101
109
|
}
|
|
102
110
|
else {
|
|
103
111
|
return undefined;
|
|
@@ -119,6 +127,7 @@ export function createSystemTokenStore(options = {}) {
|
|
|
119
127
|
}
|
|
120
128
|
};
|
|
121
129
|
return {
|
|
130
|
+
withRefreshLock: (operation, signal) => withRefreshLock(stateRoot(process.env), operation, signal),
|
|
122
131
|
get: (accountId) => { const value = active(); return value?.accountId === accountId ? value : undefined; },
|
|
123
132
|
getActive: active,
|
|
124
133
|
set: (session) => { try {
|
|
@@ -131,7 +140,10 @@ export function createSystemTokenStore(options = {}) {
|
|
|
131
140
|
try {
|
|
132
141
|
remove();
|
|
133
142
|
}
|
|
134
|
-
catch {
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (!missingCredential(platform, error))
|
|
145
|
+
throw new CliError("AUTH_ERROR", "cannot clear system credentials; check credential store permissions");
|
|
146
|
+
}
|
|
135
147
|
} },
|
|
136
148
|
clear: () => { try {
|
|
137
149
|
remove();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moewolf/moe-icons-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "Moeicons CLI: install React, Vue, Vanilla, or Assets icon targets, manage releases and login, and expose an MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -74,5 +74,5 @@
|
|
|
74
74
|
"fflate": "^0.8.2",
|
|
75
75
|
"jsonc-parser": "^3.3.1"
|
|
76
76
|
},
|
|
77
|
-
"payloadHash": "
|
|
77
|
+
"payloadHash": "39f10c2d8048f41a7fb47fade28d280399eead23a68645c7c4b6bf71243547b0"
|
|
78
78
|
}
|