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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.84",
3
+ "version": "10.0.86",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -285,7 +285,7 @@ export function DialogAuth() {
285
285
  },
286
286
  },
287
287
  {
288
- title: "Enterprise + ServiceNow",
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">("subdomain")
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 === "subdomain") {
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
- setStep("subdomain")
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
- Enterprise Portal
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 to connect to the Snow-Flow portal</text>
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>("subdomain")
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 === "subdomain") {
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
- setStep("subdomain")
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
- Enterprise + ServiceNow Setup
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: Enterprise Portal
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
- * Load token cache from disk
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 cacheData = await fs.readFile(cachePath, 'utf-8');
598
- const cached: Record<string, TokenCache> = JSON.parse(cacheData);
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
- await fs.mkdir(path.dirname(cachePath), { recursive: true });
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
- // Write cache file
634
- await fs.writeFile(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
635
- mcpDebug('[Auth] Token cache saved');
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 || '/tmp';
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