@moewolf/moe-icons-cli 0.0.6 → 0.0.8
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 +28 -2
- package/dist/auth/device-login.js +9 -1
- package/dist/auth/login.js +1 -0
- package/dist/auth/session-policy.cjs +25 -0
- package/dist/auth/token-store.js +43 -17
- package/dist/auth/transport.d.ts +2 -0
- package/dist/auth/transport.js +45 -0
- package/dist/core/auth.js +35 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -170,9 +170,10 @@ out keeps cached archives but blocks new unauthenticated installs/updates.
|
|
|
170
170
|
| `MOEICONS_BOOTSTRAP_FILE` | override bootstrap completion marker path |
|
|
171
171
|
| `MOEICONS_PRO_DESCRIPTOR_URL` | test-only Pro descriptor endpoint override (https or loopback http) |
|
|
172
172
|
| `MOEICONS_LIBRARY_VERSIONS_URL` | test-only library versions endpoint override (https or loopback http) |
|
|
173
|
-
| `MOEICONS_TOKEN_STORE_DIR` |
|
|
173
|
+
| `MOEICONS_TOKEN_STORE_DIR` | explicit file token store directory (with disabled keychain) |
|
|
174
|
+
| `MOEICONS_STATE_DIR` | session storage preference directory (default `~/.moeicons`) |
|
|
174
175
|
| `MOEICONS_DISABLE_SYSTEM_KEYCHAIN` | `1` disables OS keychain storage |
|
|
175
|
-
| `MOEICONS_AUTH0_ISSUER` / `MOEICONS_AUTH0_CLIENT_ID` | Auth0 refresh/revoke |
|
|
176
|
+
| `MOEICONS_AUTH0_ISSUER` / `MOEICONS_AUTH0_CLIENT_ID` | explicit Auth0 refresh/revoke overrides; official production defaults are bundled |
|
|
176
177
|
|
|
177
178
|
## Status
|
|
178
179
|
|
|
@@ -269,3 +270,28 @@ Available tools: `list_icon_groups`, `get_account`, `get_project_config`,
|
|
|
269
270
|
explicit unsupported operation with migration guidance. Tool failures use
|
|
270
271
|
`isError`; malformed protocol arguments use JSON-RPC errors. The server binds
|
|
271
272
|
all operations to one project root and does not expose arbitrary shell commands.
|
|
273
|
+
|
|
274
|
+
### Session storage and refresh
|
|
275
|
+
|
|
276
|
+
When native credential storage is unavailable and you approve a file store,
|
|
277
|
+
the CLI remembers that selection in `session-store.json` (no credentials).
|
|
278
|
+
Later CLI processes and the VS Code extension use the same selection. Credential
|
|
279
|
+
store errors are reported, not silently replaced by an old fallback session.
|
|
280
|
+
Log out before switching storage methods. Official production refresh uses the
|
|
281
|
+
existing public Auth0 configuration; custom/local environments must provide
|
|
282
|
+
matching issuer and client ID. Account and refresh requests have a deadline
|
|
283
|
+
covering headers and body. Revoked refresh credentials require a new login.
|
|
284
|
+
|
|
285
|
+
The VS Code extension reads credentials on its own host. For Remote SSH, WSL
|
|
286
|
+
or a container, run the CLI login on that host; local credentials are not copied.
|
|
287
|
+
|
|
288
|
+
### Independent CLI release and plugin recovery
|
|
289
|
+
|
|
290
|
+
A successful canonical publish triggers `plugin-update.yml`. It verifies the
|
|
291
|
+
published receipt and resource pin, sends a metadata-only CLI event and waits
|
|
292
|
+
for Marketplace verification. `PLUGIN_DISPATCH_TOKEN` must be configured in this
|
|
293
|
+
repository with Contents write for `moe-icons-plugins`. A failed notification is
|
|
294
|
+
replayed with `publish_run_id` in that workflow (and `published_version`
|
|
295
|
+
for the durable Release receipt after Actions artifact expiry), without republishing the CLI or
|
|
296
|
+
rebuilding resources. Temporary Actions receipts have a retention period; new
|
|
297
|
+
releases also retain a canonical receipt on their immutable GitHub Release.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLoginState, createPkcePair } from "./pkce.js";
|
|
2
2
|
import { parseCreateLoginResponse, parseExchangeLoginResponse, parsePollLoginResponse, } from "./login-schemas.js";
|
|
3
|
+
import { boundedResponse } from "./transport.js";
|
|
3
4
|
import { CliError } from "../errors/index.js";
|
|
4
5
|
function assertBrowserUrl(value, expectedOrigin) {
|
|
5
6
|
const url = new URL(value);
|
|
@@ -71,7 +72,7 @@ export async function loginWithDeviceSession(config, deps, signal) {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
export async function refreshAuth0Session(config, session, deps) {
|
|
74
|
-
const response = await deps.fetch
|
|
75
|
+
const response = await boundedResponse(deps.fetch, `${config.auth0Issuer.replace(/\/$/, "")}/oauth/token`, {
|
|
75
76
|
method: "POST",
|
|
76
77
|
headers: { "content-type": "application/json" },
|
|
77
78
|
body: JSON.stringify({
|
|
@@ -81,10 +82,14 @@ export async function refreshAuth0Session(config, session, deps) {
|
|
|
81
82
|
}),
|
|
82
83
|
...(deps.signal ? { signal: deps.signal } : {}),
|
|
83
84
|
});
|
|
85
|
+
if (response.status >= 500 || response.status === 429)
|
|
86
|
+
throw new CliError("NETWORK_ERROR", "Auth0 refresh temporarily unavailable; retry");
|
|
84
87
|
const body = (await response.json());
|
|
85
88
|
if (!response.ok || typeof body.access_token !== "string") {
|
|
86
89
|
throw new CliError("AUTH_ERROR", "Auth0 refresh failed");
|
|
87
90
|
}
|
|
91
|
+
if (body.expires_in !== undefined && (typeof body.expires_in !== "number" || !Number.isSafeInteger(body.expires_in) || body.expires_in <= 0))
|
|
92
|
+
throw new CliError("VALIDATION_ERROR", "invalid Auth0 token lifetime");
|
|
88
93
|
const updated = {
|
|
89
94
|
...session,
|
|
90
95
|
accessToken: body.access_token,
|
|
@@ -92,6 +97,9 @@ export async function refreshAuth0Session(config, session, deps) {
|
|
|
92
97
|
expiresAt: deps.now() + (typeof body.expires_in === "number" ? body.expires_in : 3600) * 1000,
|
|
93
98
|
storedAt: deps.now(),
|
|
94
99
|
};
|
|
100
|
+
const latest = deps.tokenStore.getActive();
|
|
101
|
+
if (!latest || latest.accessToken !== session.accessToken || latest.storedAt !== session.storedAt)
|
|
102
|
+
throw new CliError("AUTH_ERROR", "session changed during refresh; retry with the current account");
|
|
95
103
|
deps.tokenStore.set(updated);
|
|
96
104
|
return updated;
|
|
97
105
|
}
|
package/dist/auth/login.js
CHANGED
|
@@ -7,6 +7,7 @@ export function buildAuthorizationUrl(config, attempt) {
|
|
|
7
7
|
const params = new URLSearchParams({
|
|
8
8
|
client_id: config.clientId,
|
|
9
9
|
response_type: "code",
|
|
10
|
+
ui_locales: "en",
|
|
10
11
|
redirect_uri: `http://127.0.0.1:${attempt.port}${config.redirectPath}`,
|
|
11
12
|
scope: config.scope,
|
|
12
13
|
audience: config.audience,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Canonical CLI/extension session-store selection. Contains no credentials.
|
|
2
|
+
const {homedir} = require('node:os');
|
|
3
|
+
const {join, isAbsolute} = require('node:path');
|
|
4
|
+
const {existsSync, lstatSync, readFileSync, mkdirSync, writeFileSync, renameSync} = require('node:fs');
|
|
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
|
+
function selectedStore(env) {
|
|
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
|
+
const path = join(stateRoot(env), 'session-store.json');
|
|
9
|
+
if (!existsSync(path)) return {mode:env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === '1' ? 'none' : 'system'};
|
|
10
|
+
const meta=lstatSync(path);
|
|
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
|
+
const value=JSON.parse(readFileSync(path,'utf8'));
|
|
13
|
+
if (value.schemaVersion!==1 || value.mode!=='file' || typeof value.rootDir!=='string' || !isAbsolute(value.rootDir)) throw new Error('Invalid session storage preference');
|
|
14
|
+
return {mode:'file',rootDir:value.rootDir};
|
|
15
|
+
}
|
|
16
|
+
function rememberFileStore(env, rootDir) {
|
|
17
|
+
if (!isAbsolute(rootDir)) throw new Error('Session store requires an absolute directory');
|
|
18
|
+
const root=stateRoot(env); mkdirSync(root,{recursive:true,mode:0o700});
|
|
19
|
+
const path=join(root,'session-store.json'), temp=path+'.'+process.pid+'.tmp';
|
|
20
|
+
writeFileSync(temp,JSON.stringify({schemaVersion:1,mode:'file',rootDir})+'\n',{mode:0o600,flag:'wx'}); renameSync(temp,path);
|
|
21
|
+
}
|
|
22
|
+
function missingCredential(platform,error) {
|
|
23
|
+
return platform==='darwin' ? error.code===44 || error.status===44 : platform==='linux' ? (error.code===1 || error.status===1) && !String(error.stderr || '').trim() : platform==='win32' ? error.code===44 || error.status===44 || /0x80070490|Element not found/i.test(String(error.stderr || '')) : false;
|
|
24
|
+
}
|
|
25
|
+
module.exports={selectedStore,rememberFileStore,stateRoot,missingCredential};
|
package/dist/auth/token-store.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
1
|
+
import { existsSync, 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";
|
|
5
|
+
import { CliError } from "../errors/index.js";
|
|
4
6
|
import { execFileSync } from "node:child_process";
|
|
5
7
|
function parseSession(value) {
|
|
6
8
|
try {
|
|
7
9
|
const session = JSON.parse(value);
|
|
8
10
|
return typeof session.accountId === "string" && typeof session.accessToken === "string" &&
|
|
9
|
-
typeof session.refreshToken === "string" && typeof session.expiresAt === "number" &&
|
|
10
|
-
typeof session.scope === "string" && typeof session.storedAt === "number"
|
|
11
|
+
typeof session.refreshToken === "string" && typeof session.expiresAt === "number" && Number.isFinite(session.expiresAt) &&
|
|
12
|
+
typeof session.scope === "string" && typeof session.storedAt === "number" && Number.isFinite(session.storedAt)
|
|
11
13
|
? session : undefined;
|
|
12
14
|
}
|
|
13
15
|
catch {
|
|
@@ -25,14 +27,20 @@ export function createFileTokenStore(options = {}) {
|
|
|
25
27
|
if (!existsSync(file))
|
|
26
28
|
return {};
|
|
27
29
|
try {
|
|
30
|
+
const meta = lstatSync(file);
|
|
31
|
+
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
|
+
throw new Error("insecure store");
|
|
28
33
|
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
29
|
-
if (typeof raw
|
|
30
|
-
|
|
34
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
35
|
+
throw new Error("invalid store");
|
|
36
|
+
for (const item of Object.values(raw))
|
|
37
|
+
if (!parseSession(JSON.stringify(item)))
|
|
38
|
+
throw new Error("invalid session");
|
|
39
|
+
return raw;
|
|
31
40
|
}
|
|
32
41
|
catch {
|
|
33
|
-
|
|
42
|
+
throw new CliError("AUTH_ERROR", "file credential storage unavailable; check permissions or repair the store");
|
|
34
43
|
}
|
|
35
|
-
return {};
|
|
36
44
|
}
|
|
37
45
|
function writeAll(data) {
|
|
38
46
|
mkdirSync(root, { recursive: true });
|
|
@@ -65,7 +73,7 @@ export function createSystemTokenStore(options = {}) {
|
|
|
65
73
|
const run = options.execFile ?? execFileSync;
|
|
66
74
|
const service = "moeicons";
|
|
67
75
|
const account = "active-session";
|
|
68
|
-
const invoke = (command, args, input) => String(run(command, args, { encoding: "utf8", stdio: ["pipe", "pipe", "
|
|
76
|
+
const invoke = (command, args, input) => String(run(command, args, { encoding: "utf8", timeout: 5000, maxBuffer: 1_000_000, stdio: ["pipe", "pipe", "pipe"], ...(input ? { input } : {}) })).trim();
|
|
69
77
|
let read;
|
|
70
78
|
let write;
|
|
71
79
|
let remove;
|
|
@@ -87,23 +95,38 @@ export function createSystemTokenStore(options = {}) {
|
|
|
87
95
|
}
|
|
88
96
|
else if (platform === "win32") {
|
|
89
97
|
const prefix = "[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime];$v=[Windows.Security.Credentials.PasswordVault]::new();";
|
|
90
|
-
read = () => invoke("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `${prefix}$c=$v.Retrieve('${service}','${account}');$c.RetrievePassword();[Console]::Out.Write($c.Password)`]);
|
|
98
|
+
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}`]);
|
|
91
99
|
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); };
|
|
92
100
|
remove = () => { invoke("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `${prefix}try{$v.Remove($v.Retrieve('${service}','${account}'))}catch{}`]); };
|
|
93
101
|
}
|
|
94
102
|
else {
|
|
95
103
|
return undefined;
|
|
96
104
|
}
|
|
97
|
-
const active = () => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
const active = () => {
|
|
106
|
+
try {
|
|
107
|
+
const value = read();
|
|
108
|
+
if (!value)
|
|
109
|
+
return undefined;
|
|
110
|
+
const session = parseSession(value);
|
|
111
|
+
if (!session)
|
|
112
|
+
throw new Error("invalid session");
|
|
113
|
+
return session;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (missingCredential(platform, error))
|
|
117
|
+
return undefined;
|
|
118
|
+
throw new CliError("AUTH_ERROR", "system credential storage unavailable; retry or check its permissions");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
103
121
|
return {
|
|
104
122
|
get: (accountId) => { const value = active(); return value?.accountId === accountId ? value : undefined; },
|
|
105
123
|
getActive: active,
|
|
106
|
-
set: (session) => {
|
|
124
|
+
set: (session) => { try {
|
|
125
|
+
write(JSON.stringify(session));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new CliError("AUTH_ERROR", "cannot save system credentials; check credential store permissions");
|
|
129
|
+
} },
|
|
107
130
|
delete: (accountId) => { if (active()?.accountId === accountId) {
|
|
108
131
|
try {
|
|
109
132
|
remove();
|
|
@@ -113,7 +136,10 @@ export function createSystemTokenStore(options = {}) {
|
|
|
113
136
|
clear: () => { try {
|
|
114
137
|
remove();
|
|
115
138
|
}
|
|
116
|
-
catch {
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!missingCredential(platform, error))
|
|
141
|
+
throw new CliError("AUTH_ERROR", "cannot clear system credentials; check credential store permissions");
|
|
142
|
+
} },
|
|
117
143
|
};
|
|
118
144
|
}
|
|
119
145
|
/** Redact a session for logging: never expose tokens. */
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CliError } from "../errors/index.js";
|
|
2
|
+
/** One deadline covers headers and body; no credentials or upstream bodies in errors. */
|
|
3
|
+
export async function boundedResponse(request, url, init, timeoutMs = 10_000) {
|
|
4
|
+
const controller = new AbortController();
|
|
5
|
+
const cancel = () => controller.abort(new CliError("CANCELLED", "account request cancelled"));
|
|
6
|
+
if (init.signal?.aborted)
|
|
7
|
+
cancel();
|
|
8
|
+
else
|
|
9
|
+
init.signal?.addEventListener("abort", cancel, { once: true });
|
|
10
|
+
const timer = setTimeout(() => controller.abort(new CliError("NETWORK_ERROR", "account request timed out; retry")), timeoutMs);
|
|
11
|
+
let reader;
|
|
12
|
+
let rejectAbort = () => { };
|
|
13
|
+
const interrupted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
14
|
+
const abort = () => rejectAbort(controller.signal.reason);
|
|
15
|
+
controller.signal.addEventListener("abort", abort, { once: true });
|
|
16
|
+
try {
|
|
17
|
+
if (controller.signal.aborted)
|
|
18
|
+
throw controller.signal.reason;
|
|
19
|
+
const response = await Promise.race([request(url, { ...init, redirect: "error", signal: controller.signal }), interrupted]);
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let size = 0;
|
|
22
|
+
if (response.body) {
|
|
23
|
+
reader = response.body.getReader();
|
|
24
|
+
for (;;) {
|
|
25
|
+
const { done, value } = await Promise.race([reader.read(), interrupted]);
|
|
26
|
+
if (done)
|
|
27
|
+
break;
|
|
28
|
+
size += value.byteLength;
|
|
29
|
+
if (size > 64_000)
|
|
30
|
+
throw new CliError("VALIDATION_ERROR", "account response exceeds size limit");
|
|
31
|
+
chunks.push(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return new Response([204, 205, 304].includes(response.status) ? null : Buffer.concat(chunks), { status: response.status, headers: response.headers });
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
init.signal?.removeEventListener("abort", cancel);
|
|
39
|
+
controller.signal.removeEventListener("abort", abort);
|
|
40
|
+
if (reader) {
|
|
41
|
+
void reader.cancel().catch(() => { });
|
|
42
|
+
reader.releaseLock();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/core/auth.js
CHANGED
|
@@ -2,20 +2,36 @@ import { loginWithDeviceSession, refreshAuth0Session } from "../auth/device-logi
|
|
|
2
2
|
import { createFileTokenStore, createSystemTokenStore, redactSession } from "../auth/token-store.js";
|
|
3
3
|
import { openBrowser } from "../auth/open-browser.js";
|
|
4
4
|
import { requestJson } from "../api/client.js";
|
|
5
|
+
import { selectedStore, rememberFileStore, stateRoot } from "../auth/session-policy.cjs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { boundedResponse } from "../auth/transport.js";
|
|
5
8
|
import { CliError } from "../errors/index.js";
|
|
6
9
|
const DEFAULT_API_BASE_URL = "https://api.moeicons.com";
|
|
7
10
|
const DEFAULT_WEBSITE_ORIGIN = "https://moeicons.com";
|
|
8
11
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
9
12
|
function existingStore(context, deps) {
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
if (deps.tokenStore)
|
|
14
|
+
return deps.tokenStore;
|
|
15
|
+
const selection = selectedStore(context.env);
|
|
16
|
+
if (selection.mode === "file")
|
|
17
|
+
return (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore({ rootDir: rootDir })))(selection.rootDir);
|
|
18
|
+
if (selection.mode === "none")
|
|
19
|
+
return undefined;
|
|
20
|
+
return (deps.systemTokenStore ?? createSystemTokenStore)();
|
|
12
21
|
}
|
|
13
22
|
/** Probe existing credentials without prompting for or creating fallback storage. */
|
|
14
23
|
export async function runSessionStatusUseCase(context, deps = {}) {
|
|
15
|
-
|
|
24
|
+
let store;
|
|
25
|
+
let session;
|
|
26
|
+
try {
|
|
27
|
+
store = existingStore(context, deps);
|
|
28
|
+
session = store?.getActive();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { kind: "unknown", reason: "credential storage unavailable" };
|
|
32
|
+
}
|
|
16
33
|
if (!store)
|
|
17
34
|
return { kind: "signed-out" };
|
|
18
|
-
const session = store.getActive();
|
|
19
35
|
if (!session)
|
|
20
36
|
return { kind: "signed-out" };
|
|
21
37
|
if (session.expiresAt > context.now().getTime())
|
|
@@ -28,7 +44,7 @@ export async function runSessionStatusUseCase(context, deps = {}) {
|
|
|
28
44
|
return { kind: "authenticated", account: redactSession(refreshed) };
|
|
29
45
|
}
|
|
30
46
|
catch (error) {
|
|
31
|
-
if (!(error instanceof CliError) || error.code
|
|
47
|
+
if (!(error instanceof CliError) || error.code !== "AUTH_ERROR") {
|
|
32
48
|
return { kind: "unknown", reason: error instanceof Error ? error.message : "session validation unavailable" };
|
|
33
49
|
}
|
|
34
50
|
return { kind: "signed-out", reason: error.message };
|
|
@@ -37,6 +53,9 @@ export async function runSessionStatusUseCase(context, deps = {}) {
|
|
|
37
53
|
async function selectStore(context, deps) {
|
|
38
54
|
if (deps.tokenStore)
|
|
39
55
|
return deps.tokenStore;
|
|
56
|
+
const selected = existingStore(context, deps);
|
|
57
|
+
if (selected)
|
|
58
|
+
return selected;
|
|
40
59
|
const system = context.env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === "1" ? undefined : (deps.systemTokenStore ?? createSystemTokenStore)();
|
|
41
60
|
if (system)
|
|
42
61
|
return system;
|
|
@@ -52,7 +71,12 @@ async function selectStore(context, deps) {
|
|
|
52
71
|
throw new CliError("CANCELLED", "login cancelled");
|
|
53
72
|
if (!accepted)
|
|
54
73
|
throw new CliError("AUTH_ERROR", "login requires secure credential storage");
|
|
55
|
-
|
|
74
|
+
const rootDir = resolve(context.env.MOEICONS_TOKEN_STORE_DIR ?? stateRoot(context.env));
|
|
75
|
+
const store = (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore({ rootDir: rootDir })))(rootDir);
|
|
76
|
+
// Injected test stores never mutate the user's storage preference.
|
|
77
|
+
if (!deps.fileTokenStore)
|
|
78
|
+
rememberFileStore(context.env, rootDir);
|
|
79
|
+
return store;
|
|
56
80
|
}
|
|
57
81
|
function assertTrustedHttpBase(value, envName) {
|
|
58
82
|
let parsed;
|
|
@@ -101,7 +125,7 @@ export function resolveAuthEnvironment(env) {
|
|
|
101
125
|
}
|
|
102
126
|
const mode = declared ?? inferred;
|
|
103
127
|
// H5: the Auth0 issuer receives refresh tokens, so it must match the mode.
|
|
104
|
-
const auth0Issuer = env.MOEICONS_AUTH0_ISSUER ?? "";
|
|
128
|
+
const auth0Issuer = env.MOEICONS_AUTH0_ISSUER ?? (mode === "production" && apiBaseUrl === DEFAULT_API_BASE_URL && websiteOrigin === DEFAULT_WEBSITE_ORIGIN ? "https://login.moewolf.com/" : "");
|
|
105
129
|
if (auth0Issuer) {
|
|
106
130
|
assertTrustedHttpBase(auth0Issuer, "MOEICONS_AUTH0_ISSUER");
|
|
107
131
|
const issuerLoopback = isLoopbackHttp(auth0Issuer);
|
|
@@ -118,7 +142,7 @@ export function resolveAuthEnvironment(env) {
|
|
|
118
142
|
apiBaseUrl,
|
|
119
143
|
websiteOrigin,
|
|
120
144
|
auth0Issuer,
|
|
121
|
-
auth0ClientId: env.MOEICONS_AUTH0_CLIENT_ID ?? "",
|
|
145
|
+
auth0ClientId: env.MOEICONS_AUTH0_CLIENT_ID ?? (auth0Issuer === "https://login.moewolf.com/" ? "N7xIQMbIB1ao501jP6abai42SiWXyUXB" : ""),
|
|
122
146
|
};
|
|
123
147
|
}
|
|
124
148
|
function auth0Config(context) {
|
|
@@ -191,14 +215,8 @@ export async function runAccountUseCase(context, deps = {}) {
|
|
|
191
215
|
export async function runRemoteAccountUseCase(context, deps = {}) {
|
|
192
216
|
const apiBaseUrl = auth0Config(context).apiBaseUrl;
|
|
193
217
|
const doFetch = deps.fetch ?? fetch;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
session = (await activeSession(context, deps)).session;
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
throw new CliError("AUTH_ERROR", "not logged in");
|
|
200
|
-
}
|
|
201
|
-
const call = async (token) => doFetch(`${apiBaseUrl}/v1/cli/account`, {
|
|
218
|
+
const session = (await activeSession(context, deps)).session;
|
|
219
|
+
const call = async (token) => boundedResponse(doFetch, `${apiBaseUrl}/v1/cli/account`, {
|
|
202
220
|
method: "GET",
|
|
203
221
|
headers: { authorization: `Bearer ${token}` },
|
|
204
222
|
signal: context.signal,
|
|
@@ -235,7 +253,7 @@ export async function runLogoutUseCase(context, deps = {}) {
|
|
|
235
253
|
const config = auth0Config(context);
|
|
236
254
|
if (config.auth0Issuer && config.auth0ClientId) {
|
|
237
255
|
try {
|
|
238
|
-
const response = await (deps.fetch ?? fetch
|
|
256
|
+
const response = await boundedResponse(deps.fetch ?? fetch, `${config.auth0Issuer.replace(/\/$/, "")}/oauth/revoke`, {
|
|
239
257
|
method: "POST", headers: { "content-type": "application/json" },
|
|
240
258
|
body: JSON.stringify({ client_id: config.auth0ClientId, token: session.refreshToken }), signal: context.signal,
|
|
241
259
|
});
|
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.8",
|
|
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": "f856bfbd0728c3b91ad0b2673dfb5ec7bb67a41023922ce28a901843dd69556b"
|
|
78
78
|
}
|