snow-flow 10.0.84 → 10.0.86
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/package.json
CHANGED
|
@@ -285,7 +285,7 @@ export function DialogAuth() {
|
|
|
285
285
|
},
|
|
286
286
|
},
|
|
287
287
|
{
|
|
288
|
-
title: "
|
|
288
|
+
title: "Portal + ServiceNow",
|
|
289
289
|
value: "enterprise-combined",
|
|
290
290
|
description: isEnterpriseConfigured() && isServiceNowConfigured() ? "Both connected" : undefined,
|
|
291
291
|
category: "Combined",
|
|
@@ -919,7 +919,8 @@ function DialogAuthEnterprise() {
|
|
|
919
919
|
const toast = useToast()
|
|
920
920
|
const { theme } = useTheme()
|
|
921
921
|
|
|
922
|
-
const [step, setStep] = createSignal<"subdomain" | "code" | "verifying">("
|
|
922
|
+
const [step, setStep] = createSignal<"plan-type" | "subdomain" | "code" | "verifying">("plan-type")
|
|
923
|
+
const [planType, setPlanType] = createSignal<"individual-teams" | "enterprise" | "">("")
|
|
923
924
|
const [subdomain, setSubdomain] = createSignal("")
|
|
924
925
|
const [sessionId, setSessionId] = createSignal("")
|
|
925
926
|
const [authCode, setAuthCode] = createSignal("")
|
|
@@ -943,23 +944,50 @@ function DialogAuthEnterprise() {
|
|
|
943
944
|
} catch {
|
|
944
945
|
// Auth module not available
|
|
945
946
|
}
|
|
946
|
-
setTimeout(() => subdomainInput?.focus(), 10)
|
|
947
947
|
})
|
|
948
948
|
|
|
949
949
|
useKeyboard((evt) => {
|
|
950
950
|
if (evt.name === "escape") {
|
|
951
951
|
const currentStep = step()
|
|
952
|
-
if (currentStep === "
|
|
952
|
+
if (currentStep === "plan-type") {
|
|
953
953
|
dialog.replace(() => <DialogAuth />)
|
|
954
|
+
} else if (currentStep === "subdomain") {
|
|
955
|
+
setStep("plan-type")
|
|
956
|
+
setPlanType("")
|
|
954
957
|
} else if (currentStep === "code") {
|
|
955
|
-
|
|
958
|
+
if (planType() === "individual-teams") {
|
|
959
|
+
setStep("plan-type")
|
|
960
|
+
setPlanType("")
|
|
961
|
+
} else {
|
|
962
|
+
setStep("subdomain")
|
|
963
|
+
}
|
|
956
964
|
setSessionId("")
|
|
957
965
|
setAuthCode("")
|
|
958
966
|
setTimeout(() => subdomainInput?.focus(), 10)
|
|
959
967
|
}
|
|
960
968
|
}
|
|
969
|
+
|
|
970
|
+
// Handle 1/2 keypresses for plan type selection
|
|
971
|
+
if (step() === "plan-type") {
|
|
972
|
+
if (evt.name === "1") {
|
|
973
|
+
selectPlanType("individual-teams")
|
|
974
|
+
} else if (evt.name === "2") {
|
|
975
|
+
selectPlanType("enterprise")
|
|
976
|
+
}
|
|
977
|
+
}
|
|
961
978
|
})
|
|
962
979
|
|
|
980
|
+
const selectPlanType = (type: "individual-teams" | "enterprise") => {
|
|
981
|
+
setPlanType(type)
|
|
982
|
+
if (type === "individual-teams") {
|
|
983
|
+
setSubdomain("portal")
|
|
984
|
+
startDeviceAuth()
|
|
985
|
+
} else {
|
|
986
|
+
setStep("subdomain")
|
|
987
|
+
setTimeout(() => subdomainInput?.focus(), 10)
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
963
991
|
const startDeviceAuth = async () => {
|
|
964
992
|
const sub = subdomain().trim().toLowerCase()
|
|
965
993
|
if (!sub) {
|
|
@@ -1244,14 +1272,50 @@ function DialogAuthEnterprise() {
|
|
|
1244
1272
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
|
1245
1273
|
<box flexDirection="row" justifyContent="space-between">
|
|
1246
1274
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
1247
|
-
|
|
1275
|
+
Snow-Flow Portal
|
|
1248
1276
|
</text>
|
|
1249
1277
|
<text fg={theme.textMuted}>esc</text>
|
|
1250
1278
|
</box>
|
|
1251
1279
|
|
|
1280
|
+
<Show when={step() === "plan-type"}>
|
|
1281
|
+
<box gap={1}>
|
|
1282
|
+
<text fg={theme.textMuted}>What type of plan do you have?</text>
|
|
1283
|
+
<box paddingTop={1} gap={1}>
|
|
1284
|
+
<box
|
|
1285
|
+
flexDirection="row"
|
|
1286
|
+
gap={2}
|
|
1287
|
+
borderStyle="single"
|
|
1288
|
+
borderColor={theme.border}
|
|
1289
|
+
paddingLeft={1}
|
|
1290
|
+
paddingRight={1}
|
|
1291
|
+
>
|
|
1292
|
+
<text fg={theme.text}>[1] Individual / Teams</text>
|
|
1293
|
+
<text fg={theme.textMuted}>- Login via portal.snow-flow.dev</text>
|
|
1294
|
+
</box>
|
|
1295
|
+
<box
|
|
1296
|
+
flexDirection="row"
|
|
1297
|
+
gap={2}
|
|
1298
|
+
borderStyle="single"
|
|
1299
|
+
borderColor={theme.border}
|
|
1300
|
+
paddingLeft={1}
|
|
1301
|
+
paddingRight={1}
|
|
1302
|
+
>
|
|
1303
|
+
<text fg={theme.text}>[2] Enterprise</text>
|
|
1304
|
+
<text fg={theme.textMuted}>- Login via your organization subdomain</text>
|
|
1305
|
+
</box>
|
|
1306
|
+
</box>
|
|
1307
|
+
<box paddingTop={1} flexDirection="row">
|
|
1308
|
+
<text fg={theme.text}>1 </text>
|
|
1309
|
+
<text fg={theme.textMuted}>Individual / Teams</text>
|
|
1310
|
+
<text fg={theme.text}> 2 </text>
|
|
1311
|
+
<text fg={theme.textMuted}>Enterprise</text>
|
|
1312
|
+
</box>
|
|
1313
|
+
</box>
|
|
1314
|
+
</Show>
|
|
1315
|
+
|
|
1252
1316
|
<Show when={step() === "subdomain"}>
|
|
1253
1317
|
<box gap={1}>
|
|
1254
|
-
<text fg={theme.textMuted}>Enter your subdomain
|
|
1318
|
+
<text fg={theme.textMuted}>Enter your organization subdomain (e.g., "acme" for acme.snow-flow.dev)</text>
|
|
1255
1319
|
<textarea
|
|
1256
1320
|
ref={(val: TextareaRenderable) => (subdomainInput = val)}
|
|
1257
1321
|
height={3}
|
|
@@ -1266,10 +1330,6 @@ function DialogAuthEnterprise() {
|
|
|
1266
1330
|
startDeviceAuth()
|
|
1267
1331
|
}}
|
|
1268
1332
|
/>
|
|
1269
|
-
<text fg={theme.primary} attributes={TextAttributes.BOLD}>Individual / Teams</text>
|
|
1270
|
-
<text fg={theme.textMuted}> Enter "portal" → portal.snow-flow.dev</text>
|
|
1271
|
-
<text fg={theme.primary} attributes={TextAttributes.BOLD}>Enterprise</text>
|
|
1272
|
-
<text fg={theme.textMuted}> Enter your org name → acme.snow-flow.dev</text>
|
|
1273
1333
|
<box paddingTop={1} flexDirection="row">
|
|
1274
1334
|
<text fg={theme.text}>enter </text>
|
|
1275
1335
|
<text fg={theme.textMuted}>continue</text>
|
|
@@ -1587,6 +1647,7 @@ function DialogAuthEnterpriseCombined() {
|
|
|
1587
1647
|
const { theme } = useTheme()
|
|
1588
1648
|
|
|
1589
1649
|
type CombinedStep =
|
|
1650
|
+
| "plan-type"
|
|
1590
1651
|
| "subdomain"
|
|
1591
1652
|
| "code"
|
|
1592
1653
|
| "verifying-enterprise"
|
|
@@ -1600,7 +1661,8 @@ function DialogAuthEnterpriseCombined() {
|
|
|
1600
1661
|
| "sn-basic-password"
|
|
1601
1662
|
| "completing"
|
|
1602
1663
|
|
|
1603
|
-
const [step, setStep] = createSignal<CombinedStep>("
|
|
1664
|
+
const [step, setStep] = createSignal<CombinedStep>("plan-type")
|
|
1665
|
+
const [planType, setPlanType] = createSignal<"individual-teams" | "enterprise" | "">("")
|
|
1604
1666
|
const [subdomain, setSubdomain] = createSignal("")
|
|
1605
1667
|
const [sessionId, setSessionId] = createSignal("")
|
|
1606
1668
|
const [authCode, setAuthCode] = createSignal("")
|
|
@@ -1674,18 +1736,36 @@ function DialogAuthEnterpriseCombined() {
|
|
|
1674
1736
|
} catch {
|
|
1675
1737
|
// Auth module not available
|
|
1676
1738
|
}
|
|
1677
|
-
setTimeout(() => subdomainInput?.focus(), 10)
|
|
1678
1739
|
})
|
|
1679
1740
|
|
|
1741
|
+
const selectCombinedPlanType = (type: "individual-teams" | "enterprise") => {
|
|
1742
|
+
setPlanType(type)
|
|
1743
|
+
if (type === "individual-teams") {
|
|
1744
|
+
setSubdomain("portal")
|
|
1745
|
+
startDeviceAuth()
|
|
1746
|
+
} else {
|
|
1747
|
+
setStep("subdomain")
|
|
1748
|
+
setTimeout(() => subdomainInput?.focus(), 10)
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1680
1752
|
useKeyboard((evt) => {
|
|
1681
1753
|
const currentStep = step()
|
|
1682
1754
|
|
|
1683
1755
|
// Handle escape key for navigation
|
|
1684
1756
|
if (evt.name === "escape") {
|
|
1685
|
-
if (currentStep === "
|
|
1757
|
+
if (currentStep === "plan-type") {
|
|
1686
1758
|
dialog.replace(() => <DialogAuth />)
|
|
1759
|
+
} else if (currentStep === "subdomain") {
|
|
1760
|
+
setStep("plan-type")
|
|
1761
|
+
setPlanType("")
|
|
1687
1762
|
} else if (currentStep === "code") {
|
|
1688
|
-
|
|
1763
|
+
if (planType() === "individual-teams") {
|
|
1764
|
+
setStep("plan-type")
|
|
1765
|
+
setPlanType("")
|
|
1766
|
+
} else {
|
|
1767
|
+
setStep("subdomain")
|
|
1768
|
+
}
|
|
1689
1769
|
setSessionId("")
|
|
1690
1770
|
setAuthCode("")
|
|
1691
1771
|
setTimeout(() => subdomainInput?.focus(), 10)
|
|
@@ -1712,6 +1792,15 @@ function DialogAuthEnterpriseCombined() {
|
|
|
1712
1792
|
}
|
|
1713
1793
|
}
|
|
1714
1794
|
|
|
1795
|
+
// Handle 1/2 keypresses for plan type selection
|
|
1796
|
+
if (currentStep === "plan-type") {
|
|
1797
|
+
if (evt.name === "1") {
|
|
1798
|
+
selectCombinedPlanType("individual-teams")
|
|
1799
|
+
} else if (evt.name === "2") {
|
|
1800
|
+
selectCombinedPlanType("enterprise")
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1715
1804
|
// Handle 1/2 keypresses for ServiceNow method selection
|
|
1716
1805
|
if (currentStep === "sn-method") {
|
|
1717
1806
|
if (evt.name === "1") {
|
|
@@ -2200,11 +2289,48 @@ function DialogAuthEnterpriseCombined() {
|
|
|
2200
2289
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
|
2201
2290
|
<box flexDirection="row" justifyContent="space-between">
|
|
2202
2291
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
|
2203
|
-
|
|
2292
|
+
Portal + ServiceNow Setup
|
|
2204
2293
|
</text>
|
|
2205
2294
|
<text fg={theme.textMuted}>esc</text>
|
|
2206
2295
|
</box>
|
|
2207
2296
|
|
|
2297
|
+
{/* Plan type selection */}
|
|
2298
|
+
<Show when={step() === "plan-type"}>
|
|
2299
|
+
<box gap={1}>
|
|
2300
|
+
<text fg={theme.textMuted}>What type of plan do you have?</text>
|
|
2301
|
+
<box paddingTop={1} gap={1}>
|
|
2302
|
+
<box
|
|
2303
|
+
flexDirection="row"
|
|
2304
|
+
gap={2}
|
|
2305
|
+
borderStyle="single"
|
|
2306
|
+
borderColor={theme.border}
|
|
2307
|
+
paddingLeft={1}
|
|
2308
|
+
paddingRight={1}
|
|
2309
|
+
>
|
|
2310
|
+
<text fg={theme.text}>[1] Individual / Teams</text>
|
|
2311
|
+
<text fg={theme.textMuted}>- Login via portal.snow-flow.dev</text>
|
|
2312
|
+
</box>
|
|
2313
|
+
<box
|
|
2314
|
+
flexDirection="row"
|
|
2315
|
+
gap={2}
|
|
2316
|
+
borderStyle="single"
|
|
2317
|
+
borderColor={theme.border}
|
|
2318
|
+
paddingLeft={1}
|
|
2319
|
+
paddingRight={1}
|
|
2320
|
+
>
|
|
2321
|
+
<text fg={theme.text}>[2] Enterprise</text>
|
|
2322
|
+
<text fg={theme.textMuted}>- Login via your organization subdomain</text>
|
|
2323
|
+
</box>
|
|
2324
|
+
</box>
|
|
2325
|
+
<box paddingTop={1} flexDirection="row">
|
|
2326
|
+
<text fg={theme.text}>1 </text>
|
|
2327
|
+
<text fg={theme.textMuted}>Individual / Teams</text>
|
|
2328
|
+
<text fg={theme.text}> 2 </text>
|
|
2329
|
+
<text fg={theme.textMuted}>Enterprise</text>
|
|
2330
|
+
</box>
|
|
2331
|
+
</box>
|
|
2332
|
+
</Show>
|
|
2333
|
+
|
|
2208
2334
|
{/* Step 1: Enterprise subdomain */}
|
|
2209
2335
|
<Show when={step() === "subdomain"}>
|
|
2210
2336
|
<box gap={1}>
|
|
@@ -2237,7 +2363,7 @@ function DialogAuthEnterpriseCombined() {
|
|
|
2237
2363
|
<Show when={step() === "code"}>
|
|
2238
2364
|
<box gap={1}>
|
|
2239
2365
|
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
|
|
2240
|
-
Step 1 of 2:
|
|
2366
|
+
Step 1 of 2: Portal Login
|
|
2241
2367
|
</text>
|
|
2242
2368
|
<Show when={verificationUrl()}>
|
|
2243
2369
|
<text fg={theme.text}>Open this URL to authorize this device:</text>
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
import axios, { AxiosInstance } from 'axios';
|
|
16
16
|
import * as fs from 'fs/promises';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
+
import * as crypto from 'node:crypto';
|
|
19
|
+
import * as os from 'node:os';
|
|
18
20
|
import { ServiceNowContext, OAuthTokenResponse, EnterpriseLicense } from './types';
|
|
19
21
|
import { mcpDebug } from '../../shared/mcp-debug.js';
|
|
20
22
|
|
|
@@ -589,13 +591,73 @@ export class ServiceNowAuthManager {
|
|
|
589
591
|
}
|
|
590
592
|
|
|
591
593
|
/**
|
|
592
|
-
*
|
|
594
|
+
* Derive a machine-specific encryption key for token cache
|
|
595
|
+
*/
|
|
596
|
+
private deriveEncryptionKey(): Buffer {
|
|
597
|
+
const hostname = os.hostname();
|
|
598
|
+
let username: string;
|
|
599
|
+
try {
|
|
600
|
+
username = os.userInfo().username;
|
|
601
|
+
} catch {
|
|
602
|
+
username = process.env.USER || process.env.USERNAME || 'default';
|
|
603
|
+
}
|
|
604
|
+
const platform = os.platform();
|
|
605
|
+
const seed = `snow-flow-token-cache:${hostname}:${username}:${platform}`;
|
|
606
|
+
return crypto.createHash('sha256').update(seed).digest();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Encrypt data with AES-256-GCM
|
|
611
|
+
*/
|
|
612
|
+
private encrypt(plaintext: string): string {
|
|
613
|
+
const key = this.deriveEncryptionKey();
|
|
614
|
+
const iv = crypto.randomBytes(12);
|
|
615
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
616
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
617
|
+
const authTag = cipher.getAuthTag();
|
|
618
|
+
return JSON.stringify({
|
|
619
|
+
iv: iv.toString('base64'),
|
|
620
|
+
data: encrypted.toString('base64'),
|
|
621
|
+
tag: authTag.toString('base64'),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Decrypt data with AES-256-GCM
|
|
627
|
+
*/
|
|
628
|
+
private decrypt(ciphertext: string): string {
|
|
629
|
+
const key = this.deriveEncryptionKey();
|
|
630
|
+
const { iv, data, tag } = JSON.parse(ciphertext);
|
|
631
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
|
|
632
|
+
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
|
633
|
+
const decrypted = Buffer.concat([decipher.update(Buffer.from(data, 'base64')), decipher.final()]);
|
|
634
|
+
return decrypted.toString('utf8');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Load token cache from disk (encrypted)
|
|
593
639
|
*/
|
|
594
640
|
private async loadTokenCache(): Promise<void> {
|
|
595
641
|
try {
|
|
596
642
|
const cachePath = this.getTokenCachePath();
|
|
597
|
-
const
|
|
598
|
-
|
|
643
|
+
const raw = await fs.readFile(cachePath, 'utf-8');
|
|
644
|
+
|
|
645
|
+
let cached: Record<string, TokenCache>;
|
|
646
|
+
|
|
647
|
+
// Try decrypting first; fall back to plaintext for migration
|
|
648
|
+
try {
|
|
649
|
+
const decrypted = this.decrypt(raw);
|
|
650
|
+
cached = JSON.parse(decrypted);
|
|
651
|
+
} catch {
|
|
652
|
+
// Attempt plaintext parse for backward compatibility
|
|
653
|
+
try {
|
|
654
|
+
cached = JSON.parse(raw);
|
|
655
|
+
mcpDebug('[Auth] Migrating plaintext token cache to encrypted format');
|
|
656
|
+
} catch {
|
|
657
|
+
mcpDebug('[Auth] Token cache corrupted, starting fresh');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
599
661
|
|
|
600
662
|
// Load tokens into memory cache
|
|
601
663
|
Object.entries(cached).forEach(([key, token]) => {
|
|
@@ -605,6 +667,11 @@ export class ServiceNowAuthManager {
|
|
|
605
667
|
}
|
|
606
668
|
});
|
|
607
669
|
|
|
670
|
+
// Re-save as encrypted if we loaded from plaintext (migration)
|
|
671
|
+
if (this.tokenCache.size > 0) {
|
|
672
|
+
await this.saveTokenCache();
|
|
673
|
+
}
|
|
674
|
+
|
|
608
675
|
mcpDebug('[Auth] Loaded', this.tokenCache.size, 'cached tokens');
|
|
609
676
|
} catch (error: any) {
|
|
610
677
|
// Cache file doesn't exist or is corrupted - not critical
|
|
@@ -615,7 +682,7 @@ export class ServiceNowAuthManager {
|
|
|
615
682
|
}
|
|
616
683
|
|
|
617
684
|
/**
|
|
618
|
-
* Save token cache to disk
|
|
685
|
+
* Save token cache to disk (encrypted, restricted permissions)
|
|
619
686
|
*/
|
|
620
687
|
private async saveTokenCache(): Promise<void> {
|
|
621
688
|
try {
|
|
@@ -627,12 +694,14 @@ export class ServiceNowAuthManager {
|
|
|
627
694
|
cacheData[key] = token;
|
|
628
695
|
});
|
|
629
696
|
|
|
630
|
-
// Ensure directory exists
|
|
631
|
-
|
|
697
|
+
// Ensure directory exists with restricted permissions
|
|
698
|
+
const cacheDir = path.dirname(cachePath);
|
|
699
|
+
await fs.mkdir(cacheDir, { recursive: true, mode: 0o700 });
|
|
632
700
|
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
701
|
+
// Encrypt and write cache file with owner-only permissions
|
|
702
|
+
const encrypted = this.encrypt(JSON.stringify(cacheData));
|
|
703
|
+
await fs.writeFile(cachePath, encrypted, { encoding: 'utf-8', mode: 0o600 });
|
|
704
|
+
mcpDebug('[Auth] Token cache saved (encrypted)');
|
|
636
705
|
} catch (error: any) {
|
|
637
706
|
mcpDebug('[Auth] Failed to save cache:', error.message);
|
|
638
707
|
}
|
|
@@ -643,7 +712,7 @@ export class ServiceNowAuthManager {
|
|
|
643
712
|
*/
|
|
644
713
|
private getTokenCachePath(): string {
|
|
645
714
|
// Store in user's home directory
|
|
646
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE ||
|
|
715
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
647
716
|
return path.join(homeDir, '.snow-flow', 'token-cache.json');
|
|
648
717
|
}
|
|
649
718
|
|