@grampro/kit 2.4.3 → 2.5.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/CHANGELOG.md +169 -0
- package/README.md +12 -0
- package/index.cjs +44 -2
- package/package.json +1 -1
- package/source/components/accordion/passport.json +11 -7
- package/source/components/accordion/react/Accordion.tsx +5 -0
- package/source/components/alert/passport.json +3 -3
- package/source/components/avatar/passport.json +8 -6
- package/source/components/avatar/react/Avatar.tsx +4 -0
- package/source/components/badge/passport.json +8 -6
- package/source/components/badge/react/Badge.tsx +1 -0
- package/source/components/badge/react/Tag.tsx +1 -0
- package/source/components/breadcrumb/passport.json +8 -6
- package/source/components/breadcrumb/react/Breadcrumb.tsx +1 -0
- package/source/components/button/passport.json +8 -6
- package/source/components/button/react/Button.tsx +1 -0
- package/source/components/card/passport.json +17 -9
- package/source/components/card/react/Card.tsx +5 -0
- package/source/components/card/react/Stat.tsx +5 -0
- package/source/components/checkbox/passport.json +32 -14
- package/source/components/checkbox/react/Checkbox.tsx +17 -0
- package/source/components/checkbox/react/CheckboxGroup.tsx +6 -0
- package/source/components/combobox/passport.json +36 -16
- package/source/components/combobox/react/Select.tsx +6 -1
- package/source/components/combobox/react/props.ts +23 -0
- package/source/components/data-grid/core/types.ts +53 -0
- package/source/components/data-grid/passport.json +82 -30
- package/source/components/data-grid/react/AskGrid.tsx +10 -0
- package/source/components/data-grid/react/DataGrid.tsx +16 -0
- package/source/components/date-picker/passport.json +30 -14
- package/source/components/date-picker/react/DatePicker.tsx +5 -0
- package/source/components/date-picker/react/props.ts +13 -1
- package/source/components/dialog/core/types.ts +14 -0
- package/source/components/dialog/passport.json +26 -12
- package/source/components/dialog/react/Dialog.tsx +4 -0
- package/source/components/file-uploader/passport.json +44 -18
- package/source/components/file-uploader/react/FileUploader.tsx +39 -0
- package/source/components/input/passport.json +14 -8
- package/source/components/input/react/Input.tsx +3 -0
- package/source/components/menu/passport.json +29 -13
- package/source/components/menu/react/Menu.tsx +7 -0
- package/source/components/menu/react/items.tsx +4 -0
- package/source/components/modal/passport.json +14 -8
- package/source/components/modal/react/Modal.tsx +6 -0
- package/source/components/number-input/passport.json +23 -11
- package/source/components/number-input/react/NumberInput.tsx +13 -0
- package/source/components/popover/passport.json +17 -9
- package/source/components/popover/react/Popover.tsx +7 -0
- package/source/components/progress/passport.json +3 -3
- package/source/components/radio-group/passport.json +32 -14
- package/source/components/radio-group/react/Radio.tsx +17 -0
- package/source/components/radio-group/react/RadioGroup.tsx +13 -0
- package/source/components/skeleton/passport.json +8 -6
- package/source/components/skeleton/react/Skeleton.tsx +5 -0
- package/source/components/spinner/passport.json +3 -3
- package/source/components/switch/passport.json +23 -11
- package/source/components/switch/react/Switch.tsx +14 -0
- package/source/components/tabs/passport.json +11 -7
- package/source/components/tabs/react/Tabs.tsx +2 -0
- package/source/components/textarea/passport.json +14 -8
- package/source/components/textarea/react/Textarea.tsx +3 -0
- package/source/components/toaster/passport.json +8 -6
- package/source/components/toaster/react/Toaster.tsx +4 -0
- package/source/components/tooltip/passport.json +3 -3
- package/tools/mcp/README.md +224 -0
- package/tools/mcp/figma.cjs +445 -0
- package/tools/mcp/install.cjs +270 -0
- package/tools/mcp/secret-cli.cjs +174 -0
- package/tools/mcp/secret.cjs +289 -0
- package/tools/mcp/server.cjs +497 -0
- package/tools/passport/cli.cjs +8 -1
- package/tools/passport/extract.cjs +7 -3
- package/tools/passport/merge.cjs +24 -4
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Credentials, kept by the operating system instead of by the repository.
|
|
3
|
+
*
|
|
4
|
+
* The Figma token used to live in a `.env`, which meant a long-lived
|
|
5
|
+
* credential sitting in a file inside a git working tree — one `git add -A`
|
|
6
|
+
* away from being published. This moves it to whatever the platform already
|
|
7
|
+
* uses for secrets: the Windows Data Protection API, the macOS keychain, or
|
|
8
|
+
* the Secret Service on Linux. Each of those ties the ciphertext to the
|
|
9
|
+
* logged-in user, so a copied file is useless on another machine.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here adds a dependency. Every backend is a command the OS ships
|
|
12
|
+
* with, and the fallback is a 0600 file — worse, but still outside the repo,
|
|
13
|
+
* and it says so rather than pretending otherwise.
|
|
14
|
+
*/
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const { spawnSync } = require("child_process");
|
|
21
|
+
|
|
22
|
+
/** Namespace for keychain/Secret-Service entries. */
|
|
23
|
+
const SERVICE = "grampro-kit";
|
|
24
|
+
|
|
25
|
+
/** Per-user config directory, following each platform's own convention. */
|
|
26
|
+
function configDir() {
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), SERVICE);
|
|
29
|
+
}
|
|
30
|
+
if (process.platform === "darwin") {
|
|
31
|
+
return path.join(os.homedir(), "Library", "Application Support", SERVICE);
|
|
32
|
+
}
|
|
33
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), SERVICE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when a command exists and runs. */
|
|
37
|
+
function available(command, args) {
|
|
38
|
+
try {
|
|
39
|
+
const probe = spawnSync(command, args, { stdio: "ignore", windowsHide: true });
|
|
40
|
+
return !probe.error;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --------------------------------------------------------------- windows
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* DPAPI, reached through PowerShell because Node has no binding for it.
|
|
50
|
+
*
|
|
51
|
+
* The plaintext goes in over stdin, never as an argument — command lines are
|
|
52
|
+
* readable by other processes on the machine.
|
|
53
|
+
*
|
|
54
|
+
* The script is passed with `-Command` rather than `-EncodedCommand`, which
|
|
55
|
+
* accepts the base64 and then silently runs nothing on at least one shipped
|
|
56
|
+
* 5.1 build (5.1.22621): exit 0, no output, no error. Statements are joined
|
|
57
|
+
* with semicolons for that reason, and quoting is kept to single quotes so
|
|
58
|
+
* nothing needs escaping between here and the shell.
|
|
59
|
+
*/
|
|
60
|
+
function powershell(script, input) {
|
|
61
|
+
const run = spawnSync(
|
|
62
|
+
"powershell.exe",
|
|
63
|
+
["-NoProfile", "-NonInteractive", "-Command", script],
|
|
64
|
+
{ input: input === undefined ? "" : input, encoding: "utf8", windowsHide: true },
|
|
65
|
+
);
|
|
66
|
+
if (run.error) throw run.error;
|
|
67
|
+
if (run.status !== 0) {
|
|
68
|
+
throw new Error((run.stderr || "PowerShell failed").trim().split("\n")[0]);
|
|
69
|
+
}
|
|
70
|
+
return run.stdout;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DPAPI_PROTECT = [
|
|
74
|
+
"Add-Type -AssemblyName System.Security",
|
|
75
|
+
"$plain = [Console]::In.ReadToEnd()",
|
|
76
|
+
"$bytes = [System.Text.Encoding]::UTF8.GetBytes($plain)",
|
|
77
|
+
"[Convert]::ToBase64String([System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, 'CurrentUser'))",
|
|
78
|
+
].join("; ");
|
|
79
|
+
|
|
80
|
+
const DPAPI_UNPROTECT = [
|
|
81
|
+
"Add-Type -AssemblyName System.Security",
|
|
82
|
+
"$sealed = [Convert]::FromBase64String([Console]::In.ReadToEnd().Trim())",
|
|
83
|
+
"[System.Text.Encoding]::UTF8.GetString([System.Security.Cryptography.ProtectedData]::Unprotect($sealed, $null, 'CurrentUser'))",
|
|
84
|
+
].join("; ");
|
|
85
|
+
|
|
86
|
+
const dpapiFile = (name) => path.join(configDir(), "secrets", `${name}.dpapi`);
|
|
87
|
+
|
|
88
|
+
const dpapi = {
|
|
89
|
+
label: "Windows Data Protection API (tied to your Windows account)",
|
|
90
|
+
set(name, value) {
|
|
91
|
+
// PowerShell may fold a long string across lines when there is no console
|
|
92
|
+
// to measure, and base64 does not survive that. Whitespace is dropped on
|
|
93
|
+
// the way in and on the way out.
|
|
94
|
+
const sealed = powershell(DPAPI_PROTECT, value).replace(/\s+/g, "");
|
|
95
|
+
if (!sealed) throw new Error("DPAPI returned nothing; the secret was not stored.");
|
|
96
|
+
const file = dpapiFile(name);
|
|
97
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
fs.writeFileSync(file, sealed, { encoding: "utf8", mode: 0o600 });
|
|
99
|
+
},
|
|
100
|
+
get(name) {
|
|
101
|
+
const file = dpapiFile(name);
|
|
102
|
+
if (!fs.existsSync(file)) return null;
|
|
103
|
+
// A file carried over from another account will not decrypt; that is the
|
|
104
|
+
// protection working, so it reads as absent rather than as an error.
|
|
105
|
+
try {
|
|
106
|
+
const sealed = fs.readFileSync(file, "utf8").replace(/\s+/g, "");
|
|
107
|
+
return powershell(DPAPI_UNPROTECT, sealed).trim() || null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
remove(name) {
|
|
113
|
+
const file = dpapiFile(name);
|
|
114
|
+
if (!fs.existsSync(file)) return false;
|
|
115
|
+
fs.unlinkSync(file);
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
118
|
+
list() {
|
|
119
|
+
const dir = path.join(configDir(), "secrets");
|
|
120
|
+
if (!fs.existsSync(dir)) return [];
|
|
121
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".dpapi")).map((f) => f.slice(0, -6));
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ----------------------------------------------------------------- macos
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* The keychain. `security` takes the password as an argument, which is
|
|
129
|
+
* visible in `ps` for the moment the command runs — a real if narrow
|
|
130
|
+
* exposure, and the alternative is an interactive prompt that cannot work
|
|
131
|
+
* from a tool. Any process able to read that argument is already running as
|
|
132
|
+
* this user and could ask the keychain directly.
|
|
133
|
+
*/
|
|
134
|
+
const keychain = {
|
|
135
|
+
label: "macOS keychain",
|
|
136
|
+
set(name, value) {
|
|
137
|
+
const run = spawnSync(
|
|
138
|
+
"security",
|
|
139
|
+
["add-generic-password", "-U", "-a", os.userInfo().username, "-s", `${SERVICE}.${name}`,
|
|
140
|
+
"-D", "api token", "-j", "GramproKit", "-w", value],
|
|
141
|
+
{ encoding: "utf8" },
|
|
142
|
+
);
|
|
143
|
+
if (run.status !== 0) throw new Error((run.stderr || "security failed").trim());
|
|
144
|
+
},
|
|
145
|
+
get(name) {
|
|
146
|
+
const run = spawnSync(
|
|
147
|
+
"security",
|
|
148
|
+
["find-generic-password", "-a", os.userInfo().username, "-s", `${SERVICE}.${name}`, "-w"],
|
|
149
|
+
{ encoding: "utf8" },
|
|
150
|
+
);
|
|
151
|
+
return run.status === 0 ? run.stdout.trim() || null : null;
|
|
152
|
+
},
|
|
153
|
+
remove(name) {
|
|
154
|
+
const run = spawnSync(
|
|
155
|
+
"security",
|
|
156
|
+
["delete-generic-password", "-a", os.userInfo().username, "-s", `${SERVICE}.${name}`],
|
|
157
|
+
{ encoding: "utf8", stdio: "ignore" },
|
|
158
|
+
);
|
|
159
|
+
return run.status === 0;
|
|
160
|
+
},
|
|
161
|
+
/* The keychain has no cheap listing by service, so the known names are probed. */
|
|
162
|
+
list() {
|
|
163
|
+
return KNOWN.filter((name) => keychain.get(name) !== null);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ----------------------------------------------------------------- linux
|
|
168
|
+
|
|
169
|
+
/** libsecret's CLI, which every desktop keyring implements. */
|
|
170
|
+
const secretTool = {
|
|
171
|
+
label: "Secret Service (GNOME Keyring, KWallet)",
|
|
172
|
+
set(name, value) {
|
|
173
|
+
const run = spawnSync(
|
|
174
|
+
"secret-tool",
|
|
175
|
+
["store", "--label", `GramproKit ${name}`, "service", SERVICE, "account", name],
|
|
176
|
+
{ input: value, encoding: "utf8" },
|
|
177
|
+
);
|
|
178
|
+
if (run.status !== 0) throw new Error((run.stderr || "secret-tool failed").trim());
|
|
179
|
+
},
|
|
180
|
+
get(name) {
|
|
181
|
+
const run = spawnSync("secret-tool", ["lookup", "service", SERVICE, "account", name], {
|
|
182
|
+
encoding: "utf8",
|
|
183
|
+
});
|
|
184
|
+
return run.status === 0 ? run.stdout.trim() || null : null;
|
|
185
|
+
},
|
|
186
|
+
remove(name) {
|
|
187
|
+
const run = spawnSync("secret-tool", ["clear", "service", SERVICE, "account", name], {
|
|
188
|
+
encoding: "utf8",
|
|
189
|
+
stdio: "ignore",
|
|
190
|
+
});
|
|
191
|
+
return run.status === 0;
|
|
192
|
+
},
|
|
193
|
+
list() {
|
|
194
|
+
return KNOWN.filter((name) => secretTool.get(name) !== null);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// -------------------------------------------------------------- fallback
|
|
199
|
+
|
|
200
|
+
/*
|
|
201
|
+
* A 0600 file under the user's config directory. Not encrypted, and it says
|
|
202
|
+
* so when used — but it is outside the repository, which is the failure this
|
|
203
|
+
* whole module exists to prevent.
|
|
204
|
+
*/
|
|
205
|
+
const plainFile = {
|
|
206
|
+
label: "a 0600 file in your config directory (not encrypted)",
|
|
207
|
+
file: () => path.join(configDir(), "secrets.json"),
|
|
208
|
+
read() {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(fs.readFileSync(plainFile.file(), "utf8"));
|
|
211
|
+
} catch {
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
write(all) {
|
|
216
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
217
|
+
fs.writeFileSync(plainFile.file(), `${JSON.stringify(all, null, 2)}\n`, {
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
mode: 0o600,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
set(name, value) {
|
|
223
|
+
const all = plainFile.read();
|
|
224
|
+
all[name] = value;
|
|
225
|
+
plainFile.write(all);
|
|
226
|
+
},
|
|
227
|
+
get(name) {
|
|
228
|
+
return plainFile.read()[name] || null;
|
|
229
|
+
},
|
|
230
|
+
remove(name) {
|
|
231
|
+
const all = plainFile.read();
|
|
232
|
+
if (!(name in all)) return false;
|
|
233
|
+
delete all[name];
|
|
234
|
+
plainFile.write(all);
|
|
235
|
+
return true;
|
|
236
|
+
},
|
|
237
|
+
list() {
|
|
238
|
+
return Object.keys(plainFile.read());
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Secret names this package uses, for the backends that cannot enumerate. */
|
|
243
|
+
const KNOWN = ["figma"];
|
|
244
|
+
|
|
245
|
+
/** The best store this machine offers. Resolved once. */
|
|
246
|
+
let chosen = null;
|
|
247
|
+
function store() {
|
|
248
|
+
if (chosen) return chosen;
|
|
249
|
+
if (process.platform === "win32" && available("powershell.exe", ["-NoProfile", "-Command", "exit 0"])) {
|
|
250
|
+
chosen = dpapi;
|
|
251
|
+
} else if (process.platform === "darwin" && available("security", ["-h"])) {
|
|
252
|
+
chosen = keychain;
|
|
253
|
+
} else if (process.platform === "linux" && available("secret-tool", ["--version"])) {
|
|
254
|
+
chosen = secretTool;
|
|
255
|
+
} else {
|
|
256
|
+
chosen = plainFile;
|
|
257
|
+
}
|
|
258
|
+
return chosen;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const setSecret = (name, value) => store().set(name, value);
|
|
262
|
+
const removeSecret = (name) => store().remove(name);
|
|
263
|
+
const listSecrets = () => store().list();
|
|
264
|
+
const describeStore = () => store().label;
|
|
265
|
+
const isEncrypted = () => store() !== plainFile;
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* A secret, if this machine holds one. Never throws: a missing or unreadable
|
|
269
|
+
* store is the same answer as an empty one, because every caller has another
|
|
270
|
+
* place to look.
|
|
271
|
+
*/
|
|
272
|
+
function getSecret(name) {
|
|
273
|
+
try {
|
|
274
|
+
return store().get(name);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = {
|
|
281
|
+
setSecret,
|
|
282
|
+
getSecret,
|
|
283
|
+
removeSecret,
|
|
284
|
+
listSecrets,
|
|
285
|
+
describeStore,
|
|
286
|
+
isEncrypted,
|
|
287
|
+
configDir,
|
|
288
|
+
KNOWN,
|
|
289
|
+
};
|