@morit/cli 1.0.0
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 +26 -0
- package/assets/plugin_contract.json +136 -0
- package/bin/morit.js +11 -0
- package/package.json +41 -0
- package/src/archive.js +243 -0
- package/src/cli.js +414 -0
- package/src/cloud-client.js +89 -0
- package/src/config.js +82 -0
- package/src/secure-store.js +130 -0
- package/src/workspace.js +1630 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir, userInfo } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { normalizeApiOrigin } from "./config.js";
|
|
6
|
+
|
|
7
|
+
const credentialDirectory = join(homedir(), ".morit");
|
|
8
|
+
const credentialPath = join(credentialDirectory, "credentials.json");
|
|
9
|
+
const serviceName = "morit-cli";
|
|
10
|
+
const ACCESS_TOKEN = /^sk_live_[A-Za-z0-9]{32}$/;
|
|
11
|
+
|
|
12
|
+
function powershell(script, input = "") {
|
|
13
|
+
const result = spawnSync(
|
|
14
|
+
"powershell.exe",
|
|
15
|
+
["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
16
|
+
{ input, encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024 },
|
|
17
|
+
);
|
|
18
|
+
if (result.status !== 0) throw new Error("Windows credential protection failed");
|
|
19
|
+
return result.stdout.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function windowsProtect(token) {
|
|
23
|
+
return powershell(
|
|
24
|
+
"$v=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($v);"
|
|
25
|
+
+ "$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);"
|
|
26
|
+
+ "[Convert]::ToBase64String($p)",
|
|
27
|
+
token,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function windowsUnprotect(payload) {
|
|
32
|
+
return powershell(
|
|
33
|
+
"$v=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($v);"
|
|
34
|
+
+ "$p=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);"
|
|
35
|
+
+ "[Text.Encoding]::UTF8.GetString($p)",
|
|
36
|
+
payload,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function linuxSecretToolAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
execFileSync("secret-tool", ["--version"], { stdio: "ignore" });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function metadata() {
|
|
50
|
+
try { return JSON.parse(await readFile(credentialPath, "utf8")); }
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.code === "ENOENT") return null;
|
|
53
|
+
throw new Error("Morit credential metadata is unreadable; run morit logout and login again");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function writeMetadata(value) {
|
|
58
|
+
await mkdir(credentialDirectory, { recursive: true, mode: 0o700 });
|
|
59
|
+
const temporary = `${credentialPath}.tmp`;
|
|
60
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
61
|
+
await rename(temporary, credentialPath);
|
|
62
|
+
await chmod(credentialPath, 0o600).catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function saveCredential({ token, apiUrl, organizationId }) {
|
|
66
|
+
if (!ACCESS_TOKEN.test(token || "")) throw new Error("Developer access token is invalid");
|
|
67
|
+
const normalizedApiUrl = normalizeApiOrigin(apiUrl);
|
|
68
|
+
const account = userInfo().username;
|
|
69
|
+
if (process.platform === "win32") {
|
|
70
|
+
await writeMetadata({ version: 1, storage: "dpapi", payload: windowsProtect(token), api_url: normalizedApiUrl, organization_id: organizationId });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (process.platform === "darwin") {
|
|
74
|
+
const result = spawnSync("security", ["add-generic-password", "-a", account, "-s", serviceName, "-w", token, "-U"], {
|
|
75
|
+
stdio: "ignore",
|
|
76
|
+
});
|
|
77
|
+
if (result.status !== 0) throw new Error("Unable to save token in macOS Keychain");
|
|
78
|
+
await writeMetadata({ version: 1, storage: "keychain", account, api_url: normalizedApiUrl, organization_id: organizationId });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!linuxSecretToolAvailable()) {
|
|
82
|
+
throw new Error("A desktop keyring is required; install secret-tool or use MORIT_ACCESS_TOKEN for this session");
|
|
83
|
+
}
|
|
84
|
+
const result = spawnSync("secret-tool", ["store", "--label=Morit CLI", "service", serviceName, "account", account], {
|
|
85
|
+
input: token,
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
});
|
|
88
|
+
if (result.status !== 0) throw new Error("Unable to save token in the desktop keyring");
|
|
89
|
+
await writeMetadata({ version: 1, storage: "secret-tool", account, api_url: normalizedApiUrl, organization_id: organizationId });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function loadCredential() {
|
|
93
|
+
const environmentToken = process.env.MORIT_ACCESS_TOKEN?.trim();
|
|
94
|
+
if (environmentToken) {
|
|
95
|
+
if (!ACCESS_TOKEN.test(environmentToken)) throw new Error("MORIT_ACCESS_TOKEN is invalid");
|
|
96
|
+
return {
|
|
97
|
+
token: environmentToken,
|
|
98
|
+
apiUrl: normalizeApiOrigin(process.env.MORIT_API_URL?.trim() || "https://developers.moring.co"),
|
|
99
|
+
organizationId: process.env.MORIT_ORGANIZATION_ID?.trim() || null,
|
|
100
|
+
ephemeral: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const value = await metadata();
|
|
104
|
+
if (!value) return null;
|
|
105
|
+
const apiUrl = normalizeApiOrigin(value.api_url);
|
|
106
|
+
let token;
|
|
107
|
+
if (value.storage === "dpapi" && process.platform === "win32") {
|
|
108
|
+
token = windowsUnprotect(value.payload);
|
|
109
|
+
} else if (value.storage === "keychain" && process.platform === "darwin") {
|
|
110
|
+
token = execFileSync("security", ["find-generic-password", "-a", value.account, "-s", serviceName, "-w"], { encoding: "utf8" }).trim();
|
|
111
|
+
} else if (value.storage === "secret-tool" && linuxSecretToolAvailable()) {
|
|
112
|
+
token = execFileSync("secret-tool", ["lookup", "service", serviceName, "account", value.account], { encoding: "utf8" }).trim();
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error("Saved credentials belong to a different platform; run morit logout and login again");
|
|
115
|
+
}
|
|
116
|
+
if (!ACCESS_TOKEN.test(token)) throw new Error("Saved Morit credential is invalid");
|
|
117
|
+
return { token, apiUrl, organizationId: value.organization_id, ephemeral: false };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function clearCredential() {
|
|
121
|
+
const value = await metadata();
|
|
122
|
+
if (value?.storage === "keychain" && process.platform === "darwin") {
|
|
123
|
+
spawnSync("security", ["delete-generic-password", "-a", value.account, "-s", serviceName], { stdio: "ignore" });
|
|
124
|
+
} else if (value?.storage === "secret-tool" && linuxSecretToolAvailable()) {
|
|
125
|
+
spawnSync("secret-tool", ["clear", "service", serviceName, "account", value.account], { stdio: "ignore" });
|
|
126
|
+
}
|
|
127
|
+
await unlink(credentialPath).catch((error) => {
|
|
128
|
+
if (error.code !== "ENOENT") throw error;
|
|
129
|
+
});
|
|
130
|
+
}
|