faux-studio 0.4.0 → 0.4.2
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 +2 -2
- package/dist/index.js +182 -34
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ faux-studio runs locally and bridges your AI client to Figma Desktop:
|
|
|
50
50
|
|
|
51
51
|
## Requirements
|
|
52
52
|
|
|
53
|
-
- Node.js
|
|
53
|
+
- Node.js 20+
|
|
54
54
|
- Figma Desktop (running)
|
|
55
55
|
- Faux account (created on first auth)
|
|
56
56
|
|
|
@@ -65,7 +65,7 @@ faux-studio runs locally and bridges your AI client to Figma Desktop:
|
|
|
65
65
|
|
|
66
66
|
- Website: [faux.design](https://faux.design)
|
|
67
67
|
- Setup guide: [faux.design/docs/setup](https://faux.design/docs/setup)
|
|
68
|
-
- Issues: [github.com/
|
|
68
|
+
- Issues: [github.com/Faux-Technologies/faux-studio/issues](https://github.com/Faux-Technologies/faux-studio/issues)
|
|
69
69
|
|
|
70
70
|
## Publishing a New Version
|
|
71
71
|
|
package/dist/index.js
CHANGED
|
@@ -10414,9 +10414,6 @@ var require_dist = __commonJS({
|
|
|
10414
10414
|
});
|
|
10415
10415
|
|
|
10416
10416
|
// src/auth.ts
|
|
10417
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
10418
|
-
import { join } from "path";
|
|
10419
|
-
import { homedir } from "os";
|
|
10420
10417
|
import { spawn } from "child_process";
|
|
10421
10418
|
|
|
10422
10419
|
// src/logger.ts
|
|
@@ -10448,31 +10445,121 @@ function error(message) {
|
|
|
10448
10445
|
send("error", message);
|
|
10449
10446
|
}
|
|
10450
10447
|
|
|
10451
|
-
// src/
|
|
10448
|
+
// src/credential-store.ts
|
|
10449
|
+
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
10450
|
+
import { join } from "path";
|
|
10451
|
+
import { homedir } from "os";
|
|
10452
10452
|
var FAUX_DIR = join(homedir(), ".faux");
|
|
10453
10453
|
var CREDENTIALS_PATH = join(FAUX_DIR, "credentials.json");
|
|
10454
|
-
var
|
|
10455
|
-
var
|
|
10456
|
-
var
|
|
10457
|
-
|
|
10458
|
-
async
|
|
10454
|
+
var KEYCHAIN_SERVICE = "faux-studio";
|
|
10455
|
+
var KEYCHAIN_ACCOUNT = "credentials";
|
|
10456
|
+
var FileCredentialStore = class {
|
|
10457
|
+
name = "file";
|
|
10458
|
+
async load() {
|
|
10459
|
+
try {
|
|
10460
|
+
const raw = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
10461
|
+
const creds = JSON.parse(raw);
|
|
10462
|
+
if (!creds.jwt || !creds.refreshToken || !creds.user) return null;
|
|
10463
|
+
return creds;
|
|
10464
|
+
} catch {
|
|
10465
|
+
return null;
|
|
10466
|
+
}
|
|
10467
|
+
}
|
|
10468
|
+
async save(creds) {
|
|
10469
|
+
try {
|
|
10470
|
+
await mkdir(FAUX_DIR, { recursive: true });
|
|
10471
|
+
await writeFile(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
10472
|
+
} catch (err) {
|
|
10473
|
+
warn(`Could not save credentials: ${err instanceof Error ? err.message : err}`);
|
|
10474
|
+
}
|
|
10475
|
+
}
|
|
10476
|
+
async clear() {
|
|
10477
|
+
try {
|
|
10478
|
+
await unlink(CREDENTIALS_PATH);
|
|
10479
|
+
} catch {
|
|
10480
|
+
}
|
|
10481
|
+
}
|
|
10482
|
+
};
|
|
10483
|
+
var KeychainCredentialStore = class {
|
|
10484
|
+
name = "keychain";
|
|
10485
|
+
entry;
|
|
10486
|
+
constructor(entry) {
|
|
10487
|
+
this.entry = entry;
|
|
10488
|
+
}
|
|
10489
|
+
async load() {
|
|
10490
|
+
try {
|
|
10491
|
+
const raw = await this.entry.getPassword();
|
|
10492
|
+
if (!raw) return null;
|
|
10493
|
+
const creds = JSON.parse(raw);
|
|
10494
|
+
if (!creds.jwt || !creds.refreshToken || !creds.user) return null;
|
|
10495
|
+
return creds;
|
|
10496
|
+
} catch {
|
|
10497
|
+
return null;
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
async save(creds) {
|
|
10501
|
+
try {
|
|
10502
|
+
await this.entry.setPassword(JSON.stringify(creds));
|
|
10503
|
+
} catch (err) {
|
|
10504
|
+
warn(`Could not save to keychain: ${err instanceof Error ? err.message : err}`);
|
|
10505
|
+
}
|
|
10506
|
+
}
|
|
10507
|
+
async clear() {
|
|
10508
|
+
try {
|
|
10509
|
+
await this.entry.deletePassword();
|
|
10510
|
+
} catch {
|
|
10511
|
+
}
|
|
10512
|
+
}
|
|
10513
|
+
};
|
|
10514
|
+
async function tryCreateKeychainStore() {
|
|
10459
10515
|
try {
|
|
10460
|
-
const
|
|
10461
|
-
const
|
|
10462
|
-
|
|
10463
|
-
return
|
|
10464
|
-
} catch {
|
|
10516
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
10517
|
+
const entry = new AsyncEntry(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
|
|
10518
|
+
await entry.getPassword();
|
|
10519
|
+
return new KeychainCredentialStore(entry);
|
|
10520
|
+
} catch (err) {
|
|
10521
|
+
warn(
|
|
10522
|
+
`OS keychain not available, using file-based credential storage: ${err instanceof Error ? err.message : err}`
|
|
10523
|
+
);
|
|
10465
10524
|
return null;
|
|
10466
10525
|
}
|
|
10467
10526
|
}
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
}
|
|
10473
|
-
|
|
10527
|
+
var initPromise = null;
|
|
10528
|
+
function createCredentialStore() {
|
|
10529
|
+
if (!initPromise) {
|
|
10530
|
+
initPromise = initStore();
|
|
10531
|
+
}
|
|
10532
|
+
return initPromise;
|
|
10533
|
+
}
|
|
10534
|
+
async function initStore() {
|
|
10535
|
+
const fileStore = new FileCredentialStore();
|
|
10536
|
+
const keychainStore = await tryCreateKeychainStore();
|
|
10537
|
+
if (!keychainStore) {
|
|
10538
|
+
return fileStore;
|
|
10539
|
+
}
|
|
10540
|
+
const keychainCreds = await keychainStore.load();
|
|
10541
|
+
if (!keychainCreds) {
|
|
10542
|
+
const fileCreds = await fileStore.load();
|
|
10543
|
+
if (fileCreds) {
|
|
10544
|
+
await keychainStore.save(fileCreds);
|
|
10545
|
+
const verified = await keychainStore.load();
|
|
10546
|
+
if (verified) {
|
|
10547
|
+
await fileStore.clear();
|
|
10548
|
+
log("Credentials migrated to OS keychain");
|
|
10549
|
+
} else {
|
|
10550
|
+
warn("Keychain migration could not be verified \u2014 keeping file-based credentials");
|
|
10551
|
+
return fileStore;
|
|
10552
|
+
}
|
|
10553
|
+
}
|
|
10474
10554
|
}
|
|
10555
|
+
return keychainStore;
|
|
10475
10556
|
}
|
|
10557
|
+
|
|
10558
|
+
// src/auth.ts
|
|
10559
|
+
var AUTH_BASE = process.env.FAUX_AUTH_URL || "https://auth.faux.design";
|
|
10560
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
10561
|
+
var POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
10562
|
+
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
10476
10563
|
function isExpiringSoon(creds) {
|
|
10477
10564
|
const expiresAt = new Date(creds.expiresAt).getTime();
|
|
10478
10565
|
return Date.now() > expiresAt - REFRESH_BUFFER_MS;
|
|
@@ -10543,7 +10630,7 @@ async function authenticate() {
|
|
|
10543
10630
|
expiresAt: new Date(Date.now() + 3600 * 1e3).toISOString(),
|
|
10544
10631
|
user: data.user
|
|
10545
10632
|
};
|
|
10546
|
-
await
|
|
10633
|
+
await (await createCredentialStore()).save(creds);
|
|
10547
10634
|
return creds;
|
|
10548
10635
|
}
|
|
10549
10636
|
if (data.status === "error") {
|
|
@@ -10570,7 +10657,8 @@ async function ensureAuth() {
|
|
|
10570
10657
|
source: "api-key"
|
|
10571
10658
|
};
|
|
10572
10659
|
}
|
|
10573
|
-
const
|
|
10660
|
+
const credStore = await createCredentialStore();
|
|
10661
|
+
const saved = await credStore.load();
|
|
10574
10662
|
if (saved) {
|
|
10575
10663
|
if (!isExpiringSoon(saved)) {
|
|
10576
10664
|
log(`Authenticated as ${saved.user.handle}`);
|
|
@@ -10584,7 +10672,7 @@ async function ensureAuth() {
|
|
|
10584
10672
|
try {
|
|
10585
10673
|
log("Refreshing authentication...");
|
|
10586
10674
|
const refreshed = await refreshJwt(saved);
|
|
10587
|
-
await
|
|
10675
|
+
await credStore.save(refreshed);
|
|
10588
10676
|
log(`Authenticated as ${refreshed.user.handle}`);
|
|
10589
10677
|
return {
|
|
10590
10678
|
jwt: refreshed.jwt,
|
|
@@ -10607,12 +10695,13 @@ async function ensureAuth() {
|
|
|
10607
10695
|
}
|
|
10608
10696
|
async function refreshIfNeeded(current, force = false) {
|
|
10609
10697
|
if (current.source === "api-key") return current;
|
|
10610
|
-
const
|
|
10698
|
+
const credStore = await createCredentialStore();
|
|
10699
|
+
const saved = await credStore.load();
|
|
10611
10700
|
if (!saved) return current;
|
|
10612
10701
|
if (!force && !isExpiringSoon(saved)) return current;
|
|
10613
10702
|
try {
|
|
10614
10703
|
const refreshed = await refreshJwt(saved);
|
|
10615
|
-
await
|
|
10704
|
+
await credStore.save(refreshed);
|
|
10616
10705
|
return {
|
|
10617
10706
|
jwt: refreshed.jwt,
|
|
10618
10707
|
refreshToken: refreshed.refreshToken,
|
|
@@ -10628,9 +10717,34 @@ async function refreshIfNeeded(current, force = false) {
|
|
|
10628
10717
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
10629
10718
|
import { join as join2 } from "path";
|
|
10630
10719
|
import { homedir as homedir2 } from "os";
|
|
10720
|
+
import { subtle } from "crypto";
|
|
10631
10721
|
var FAUX_DIR2 = join2(homedir2(), ".faux");
|
|
10632
10722
|
var CACHE_PATH = join2(FAUX_DIR2, "tool-cache.json");
|
|
10633
10723
|
var API_BASE = process.env.FAUX_API_URL || "https://api.faux.design";
|
|
10724
|
+
var _publicKey = null;
|
|
10725
|
+
var _cachedVerifyKey = null;
|
|
10726
|
+
var _cachedKeyInput = null;
|
|
10727
|
+
function getVerifyKey() {
|
|
10728
|
+
return _publicKey;
|
|
10729
|
+
}
|
|
10730
|
+
async function importPublicKey(jwkJson) {
|
|
10731
|
+
if (_cachedVerifyKey && _cachedKeyInput === jwkJson) return _cachedVerifyKey;
|
|
10732
|
+
const jwk = JSON.parse(jwkJson);
|
|
10733
|
+
_cachedVerifyKey = await subtle.importKey("jwk", jwk, { name: "Ed25519" }, false, ["verify"]);
|
|
10734
|
+
_cachedKeyInput = jwkJson;
|
|
10735
|
+
return _cachedVerifyKey;
|
|
10736
|
+
}
|
|
10737
|
+
async function verifyScriptSignature(script, signature, keyJwk) {
|
|
10738
|
+
if (signature.length !== 128) return false;
|
|
10739
|
+
try {
|
|
10740
|
+
const key = await importPublicKey(keyJwk);
|
|
10741
|
+
const sigBytes = Uint8Array.from(signature.match(/.{2}/g).map((h) => parseInt(h, 16)));
|
|
10742
|
+
const scriptBytes = new TextEncoder().encode(script);
|
|
10743
|
+
return await subtle.verify("Ed25519", key, sigBytes, scriptBytes);
|
|
10744
|
+
} catch {
|
|
10745
|
+
return false;
|
|
10746
|
+
}
|
|
10747
|
+
}
|
|
10634
10748
|
async function loadCachedTools() {
|
|
10635
10749
|
try {
|
|
10636
10750
|
const raw = await readFile2(CACHE_PATH, "utf-8");
|
|
@@ -10679,6 +10793,9 @@ async function fetchRemoteTools(jwt2) {
|
|
|
10679
10793
|
throw new Error(`Failed to fetch tools (HTTP ${res.status})`);
|
|
10680
10794
|
}
|
|
10681
10795
|
const data = await res.json();
|
|
10796
|
+
if (data.signingKey) {
|
|
10797
|
+
_publicKey = data.signingKey;
|
|
10798
|
+
}
|
|
10682
10799
|
return sanitizeTools(data.tools);
|
|
10683
10800
|
}
|
|
10684
10801
|
async function getTools(jwt2) {
|
|
@@ -10732,7 +10849,7 @@ async function generateScript(jwt2, toolName, params) {
|
|
|
10732
10849
|
throw new Error(body.error || `Script generation failed (HTTP ${res.status})`);
|
|
10733
10850
|
}
|
|
10734
10851
|
const data = await res.json();
|
|
10735
|
-
return data.script;
|
|
10852
|
+
return { script: data.script, signature: data.signature };
|
|
10736
10853
|
}
|
|
10737
10854
|
|
|
10738
10855
|
// node_modules/ws/wrapper.mjs
|
|
@@ -25819,7 +25936,7 @@ Resources provide quick read-only access to Figma state without tool calls:
|
|
|
25819
25936
|
- Create components for reusable UI patterns.`;
|
|
25820
25937
|
function createMcpServer(deps) {
|
|
25821
25938
|
const server2 = new Server(
|
|
25822
|
-
{ name: "faux-studio", version: "0.
|
|
25939
|
+
{ name: "faux-studio", version: "0.4.2" },
|
|
25823
25940
|
{
|
|
25824
25941
|
capabilities: { tools: { listChanged: true }, resources: {}, logging: {} },
|
|
25825
25942
|
instructions: INSTRUCTIONS
|
|
@@ -25884,11 +26001,13 @@ function createMcpServer(deps) {
|
|
|
25884
26001
|
}) }]
|
|
25885
26002
|
};
|
|
25886
26003
|
} catch (err) {
|
|
26004
|
+
const transport = deps.getTransport();
|
|
26005
|
+
const restartHint = transport === "cdp" ? "Please disable and re-enable the faux-studio MCP server in your MCP client settings, then try again." : transport === "plugin" ? "Please close and reopen the faux-studio plugin in Figma, then try again." : "Please restart faux-studio and try again.";
|
|
25887
26006
|
return {
|
|
25888
26007
|
content: [{ type: "text", text: JSON.stringify({
|
|
25889
26008
|
authenticated: false,
|
|
25890
26009
|
error: err instanceof Error ? err.message : "Re-authentication failed",
|
|
25891
|
-
message:
|
|
26010
|
+
message: `Could not authenticate. ${restartHint}`
|
|
25892
26011
|
}) }],
|
|
25893
26012
|
isError: true
|
|
25894
26013
|
};
|
|
@@ -25976,12 +26095,23 @@ function createMcpServer(deps) {
|
|
|
25976
26095
|
}
|
|
25977
26096
|
const message = err instanceof Error ? err.message : String(err);
|
|
25978
26097
|
const isAuthError = message.includes("auth") || message.includes("401") || message.includes("AUTH_EXPIRED");
|
|
26098
|
+
if (isAuthError) {
|
|
26099
|
+
const transport = deps.getTransport();
|
|
26100
|
+
const reconnectHint = transport === "cdp" ? "If the `login` tool fails, ask the user to disable and re-enable the faux-studio MCP server in their MCP client settings, then try again." : transport === "plugin" ? "If the `login` tool fails, ask the user to close and reopen the faux-studio plugin in Figma, then try again." : "If the `login` tool fails, ask the user to restart faux-studio and try again.";
|
|
26101
|
+
return {
|
|
26102
|
+
content: [{
|
|
26103
|
+
type: "text",
|
|
26104
|
+
text: `Error: ${message}
|
|
26105
|
+
|
|
26106
|
+
Run the \`login\` tool to re-authenticate. ${reconnectHint}`
|
|
26107
|
+
}],
|
|
26108
|
+
isError: true
|
|
26109
|
+
};
|
|
26110
|
+
}
|
|
25979
26111
|
return {
|
|
25980
26112
|
content: [{
|
|
25981
26113
|
type: "text",
|
|
25982
|
-
text:
|
|
25983
|
-
|
|
25984
|
-
Run the \`login\` tool to re-authenticate.` : `Error: ${message}`
|
|
26114
|
+
text: `Error: ${message}`
|
|
25985
26115
|
}],
|
|
25986
26116
|
isError: true
|
|
25987
26117
|
};
|
|
@@ -26112,16 +26242,29 @@ async function recoverCdp(client, script) {
|
|
|
26112
26242
|
}
|
|
26113
26243
|
async function generateWithAuth(toolName, params) {
|
|
26114
26244
|
auth = await refreshIfNeeded(auth);
|
|
26245
|
+
let result;
|
|
26115
26246
|
try {
|
|
26116
|
-
|
|
26247
|
+
result = await generateScript(auth.jwt, toolName, params);
|
|
26117
26248
|
} catch (err) {
|
|
26118
26249
|
if (err instanceof Error && err.message === "AUTH_EXPIRED") {
|
|
26119
26250
|
warn("JWT expired during request, refreshing...");
|
|
26120
26251
|
auth = await refreshIfNeeded(auth, true);
|
|
26121
|
-
|
|
26252
|
+
result = await generateScript(auth.jwt, toolName, params);
|
|
26253
|
+
} else {
|
|
26254
|
+
throw err;
|
|
26255
|
+
}
|
|
26256
|
+
}
|
|
26257
|
+
const verifyKey = getVerifyKey();
|
|
26258
|
+
if (verifyKey && result.signature) {
|
|
26259
|
+
if (!await verifyScriptSignature(result.script, result.signature, verifyKey)) {
|
|
26260
|
+
throw new Error("Script signature verification failed \u2014 possible tampering");
|
|
26122
26261
|
}
|
|
26123
|
-
|
|
26262
|
+
} else if (verifyKey && !result.signature) {
|
|
26263
|
+
throw new Error("Signing key is configured but response is missing a signature \u2014 possible signature stripping attack");
|
|
26264
|
+
} else if (!verifyKey) {
|
|
26265
|
+
warn("No signing key available \u2014 skipping script signature verification");
|
|
26124
26266
|
}
|
|
26267
|
+
return result.script;
|
|
26125
26268
|
}
|
|
26126
26269
|
var PLUGIN_URL = "https://faux.design/plugin";
|
|
26127
26270
|
var PLUGIN_WAIT_MS = 1e4;
|
|
@@ -26303,6 +26446,11 @@ async function main() {
|
|
|
26303
26446
|
source: auth.source,
|
|
26304
26447
|
isApiKey: auth.source === "api-key"
|
|
26305
26448
|
}),
|
|
26449
|
+
getTransport: () => {
|
|
26450
|
+
if (pluginServer.hasConnections) return "plugin";
|
|
26451
|
+
if (cdpClient?.connected) return "cdp";
|
|
26452
|
+
return "none";
|
|
26453
|
+
},
|
|
26306
26454
|
setupFigma
|
|
26307
26455
|
});
|
|
26308
26456
|
await startServer(server2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faux-studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "AI-powered Figma design via MCP — connect any AI client to Figma Desktop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=20"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsup",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"build:plugin": "cd plugin && npm run build",
|
|
19
19
|
"build:all": "npm run build && npm run build:plugin",
|
|
20
20
|
"dev": "tsup --watch",
|
|
21
|
+
"test": "npx tsx --test test/*.test.ts",
|
|
21
22
|
"prepublishOnly": "npm run build"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"@types/node": "^22.0.0",
|
|
27
28
|
"@types/ws": "^8.5.0",
|
|
28
29
|
"tsup": "^8.4.0",
|
|
30
|
+
"tsx": "^4.19.0",
|
|
29
31
|
"typescript": "^5.8.0"
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
@@ -41,5 +43,8 @@
|
|
|
41
43
|
"type": "git",
|
|
42
44
|
"url": "https://github.com/uxfreak/faux-studio.git"
|
|
43
45
|
},
|
|
44
|
-
"homepage": "https://faux.design"
|
|
46
|
+
"homepage": "https://faux.design",
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"@napi-rs/keyring": "^1.2.0"
|
|
49
|
+
}
|
|
45
50
|
}
|