[object Object]

← back to Ventura Claw

preload fleet: 5 seed users × 10 connectors from secrets-manager .env

01b8b0571f7bb09d0348594efd7b3ad20ec4ed4d · 2026-05-24 20:51:57 -0700 · SteveStudio2

- maskFields: first4 + 8 asterisks + last4 (was first4…last4); pass _* provenance through
- scripts/preload-fleet.js: full ENV_KEY_TO_CONNECTOR map, 5 canonical logins
  (admin, info@vcl, steve@vcl, info@dw, steve@dw — all role=admin, bcrypt-hashed)
- Backs up existing encrypted credentials.json; writes plaintext seed
  (server boot re-encrypts under CC_SECRET via existing migration block)

Files touched

Diff

commit 01b8b0571f7bb09d0348594efd7b3ad20ec4ed4d
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Sun May 24 20:51:57 2026 -0700

    preload fleet: 5 seed users × 10 connectors from secrets-manager .env
    
    - maskFields: first4 + 8 asterisks + last4 (was first4…last4); pass _* provenance through
    - scripts/preload-fleet.js: full ENV_KEY_TO_CONNECTOR map, 5 canonical logins
      (admin, info@vcl, steve@vcl, info@dw, steve@dw — all role=admin, bcrypt-hashed)
    - Backs up existing encrypted credentials.json; writes plaintext seed
      (server boot re-encrypts under CC_SECRET via existing migration block)
---
 scripts/preload-fleet.js | 215 +++++++++++++++++++++++++++++++++++++++++++++++
 server/server.js         |   3 +-
 2 files changed, 217 insertions(+), 1 deletion(-)

diff --git a/scripts/preload-fleet.js b/scripts/preload-fleet.js
new file mode 100755
index 0000000..48fa61b
--- /dev/null
+++ b/scripts/preload-fleet.js
@@ -0,0 +1,215 @@
+#!/usr/bin/env node
+// preload-fleet.js
+// Seeds users.json + credentials.json so the platform is "67 connectors pre-wired,
+// ready on day one" for five canonical logins: admin, info@dw, steve@dw, info@vcl,
+// steve@vcl. Pulls real values from ~/Projects/secrets-manager/.env, maps via the
+// same ENV_KEY_TO_CONNECTOR table the live server uses for /api/me/connections/import,
+// and writes plaintext credentials.json. The server's boot-migration block re-encrypts
+// it at-rest on next start (AES-256-GCM under CC_SECRET).
+//
+//   node scripts/preload-fleet.js              # dry-run (default)
+//   node scripts/preload-fleet.js --write      # commit changes
+//
+// Existing encrypted credentials.json is moved to credentials.json.bak.<ts> first.
+
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const bcrypt = require(require.resolve('bcrypt', { paths: [path.join(__dirname, '..', 'server')] }));
+
+const ROOT = path.join(__dirname, '..', 'server');
+const DATA = path.join(ROOT, 'data');
+const SECRETS_ENV = path.join(process.env.HOME, 'Projects/secrets-manager/.env');
+const USERS_JSON = path.join(DATA, 'users.json');
+const CREDS_JSON = path.join(DATA, 'credentials.json');
+
+// Same map the live server uses (server.js line 1931). Duplicated here so this
+// script doesn't have to import server.js (which would boot it). Keep in sync.
+const ENV_KEY_TO_CONNECTOR = {
+  STRIPE_SECRET_KEY:        { connector: 'stripe',      field: 'STRIPE_SECRET_KEY' },
+  STRIPE_PUBLISHABLE_KEY:   { connector: 'stripe',      field: 'STRIPE_PUBLISHABLE_KEY' },
+  STRIPE_API_KEY:           { connector: 'stripe',      field: 'STRIPE_SECRET_KEY' },
+  CLOUDFLARE_API_TOKEN:     { connector: 'cloudflare',  field: 'CF_API_TOKEN' },
+  CLOUDFLARE_ACCOUNT_ID:    { connector: 'cloudflare',  field: 'CF_ACCOUNT_ID' },
+  CLOUDFLARE_TOKEN:         { connector: 'cloudflare',  field: 'CF_API_TOKEN' },
+  GODADDY_API_KEY:          { connector: 'godaddy',     field: 'GODADDY_API_KEY' },
+  GODADDY_API_SECRET:       { connector: 'godaddy',     field: 'GODADDY_API_SECRET' },
+  BROWSERBASE_API_KEY:      { connector: 'browserbase', field: 'BROWSERBASE_API_KEY' },
+  BROWSERBASE_PROJECT_ID:   { connector: 'browserbase', field: 'BROWSERBASE_PROJECT_ID' },
+  SHOPIFY_ACCESS_TOKEN:     { connector: 'shopify',     field: 'SHOPIFY_ADMIN_TOKEN' },
+  SHOPIFY_ADMIN_TOKEN:      { connector: 'shopify',     field: 'SHOPIFY_ADMIN_TOKEN' },
+  SHOPIFY_STORE:            { connector: 'shopify',     field: 'SHOPIFY_STORE' },
+  SHOPIFY_TOKEN:            { connector: 'shopify',     field: 'SHOPIFY_ADMIN_TOKEN' },
+  ETSY_API_KEY:             { connector: 'etsy',        field: 'ETSY_KEYSTRING' },
+  ETSY_KEYSTRING:           { connector: 'etsy',        field: 'ETSY_KEYSTRING' },
+  ETSY_ACCESS_TOKEN:        { connector: 'etsy',        field: 'ETSY_ACCESS_TOKEN' },
+  PAYPAL_CLIENT_ID:         { connector: 'paypal',      field: 'PAYPAL_CLIENT_ID' },
+  PAYPAL_SECRET:            { connector: 'paypal',      field: 'PAYPAL_SECRET' },
+  SQUARE_ACCESS_TOKEN:      { connector: 'square',      field: 'SQUARE_ACCESS_TOKEN' },
+  TWILIO_ACCOUNT_SID:       { connector: 'twilio',      field: 'TWILIO_ACCOUNT_SID' },
+  TWILIO_AUTH_TOKEN:        { connector: 'twilio',      field: 'TWILIO_AUTH_TOKEN' },
+  TWILIO_PHONE_NUMBER:      { connector: 'twilio',      field: 'TWILIO_FROM_NUMBER' },
+  TWILIO_FROM_NUMBER:       { connector: 'twilio',      field: 'TWILIO_FROM_NUMBER' },
+  SLACK_BOT_TOKEN:          { connector: 'slack',       field: 'SLACK_BOT_TOKEN' },
+  SLACK_TOKEN:              { connector: 'slack',       field: 'SLACK_BOT_TOKEN' },
+  DISCORD_BOT_TOKEN:        { connector: 'discord',     field: 'DISCORD_BOT_TOKEN' },
+  MAILCHIMP_API_KEY:        { connector: 'mailchimp',   field: 'MAILCHIMP_API_KEY' },
+  KLAVIYO_API_KEY:          { connector: 'klaviyo',     field: 'KLAVIYO_API_KEY' },
+  HUBSPOT_ACCESS_TOKEN:     { connector: 'hubspot',     field: 'HUBSPOT_TOKEN' },
+  HUBSPOT_TOKEN:            { connector: 'hubspot',     field: 'HUBSPOT_TOKEN' },
+  SALESFORCE_ACCESS_TOKEN:  { connector: 'salesforce',  field: 'SALESFORCE_ACCESS_TOKEN' },
+  ZENDESK_API_TOKEN:        { connector: 'zendesk',     field: 'ZENDESK_API_TOKEN' },
+  ZENDESK_WIDGET_KEY:       { connector: 'zendesk',     field: 'ZENDESK_WIDGET_KEY' },
+  INTERCOM_ACCESS_TOKEN:    { connector: 'intercom',    field: 'INTERCOM_ACCESS_TOKEN' },
+  NOTION_API_KEY:           { connector: 'notion',      field: 'NOTION_TOKEN' },
+  NOTION_TOKEN:             { connector: 'notion',      field: 'NOTION_TOKEN' },
+  AIRTABLE_PAT:             { connector: 'airtable',    field: 'AIRTABLE_PAT' },
+  AIRTABLE_TOKEN:           { connector: 'airtable',    field: 'AIRTABLE_PAT' },
+  AIRTABLE_BASE_ID:         { connector: 'airtable',    field: 'AIRTABLE_BASE_ID' },
+  ASANA_PAT:                { connector: 'asana',       field: 'ASANA_PAT' },
+  CLICKUP_API_TOKEN:        { connector: 'clickup',     field: 'CLICKUP_API_TOKEN' },
+  TRELLO_API_KEY:           { connector: 'trello',      field: 'TRELLO_API_KEY' },
+  TRELLO_TOKEN:             { connector: 'trello',      field: 'TRELLO_TOKEN' },
+  FIGMA_TOKEN:              { connector: 'figma',       field: 'FIGMA_TOKEN' },
+  FIGMA_API_TOKEN:          { connector: 'figma',       field: 'FIGMA_TOKEN' },
+  CANVA_API_KEY:            { connector: 'canva',       field: 'CANVA_TOKEN' },
+  CANVA_TOKEN:              { connector: 'canva',       field: 'CANVA_TOKEN' },
+  ADOBE_CLIENT_ID:          { connector: 'adobe',       field: 'ADOBE_CLIENT_ID' },
+  AWS_ACCESS_KEY_ID:        { connector: 'aws',         field: 'AWS_ACCESS_KEY_ID' },
+  AWS_SECRET_ACCESS_KEY:    { connector: 'aws',         field: 'AWS_SECRET_ACCESS_KEY' },
+  VERCEL_TOKEN:             { connector: 'vercel',      field: 'VERCEL_TOKEN' },
+  SUPABASE_KEY:             { connector: 'supabase',    field: 'SUPABASE_KEY' },
+  ELEVENLABS_API_KEY:       { connector: 'elevenlabs',  field: 'ELEVENLABS_API_KEY' },
+  PURELYMAIL_API_TOKEN:     { connector: 'purelymail',  field: 'PURELYMAIL_API_TOKEN' },
+  LINKEDIN_CLIENT_ID:       { connector: 'linkedin',    field: 'LINKEDIN_CLIENT_ID' },
+  LINKEDIN_CLIENT_SECRET:   { connector: 'linkedin',    field: 'LINKEDIN_CLIENT_SECRET' },
+};
+
+// 5 canonical logins. Per Steve's "preload as admin, info@dw, steve@dw" + the
+// "Both" choice on the @dw vs @vcl clarifier — seed both pairs so any natural
+// login attempt lands.
+function userSeeds(env) {
+  const dwPass = env.X_DEFAULT_PASSWORD || 'dw-demo-please-rotate';
+  return [
+    // admin keeps the existing stable id=1; password from env or 'admin' fallback
+    { id: 1,                email: 'admin@venturaclaw.com',           role: 'admin', name: 'Admin',                       password: env.VENTURACLAW_ADMIN_PASSWORD || 'admin' },
+    { id: 1778002313860,    email: 'steve@venturaclaw.com',           role: 'admin', name: 'Steve Abrams',                password: env.VENTURACLAW_ADMIN_PASSWORD || 'admin' },
+    { id: 1779000000001,    email: 'info@venturaclaw.com',            role: 'admin', name: 'Info — VenturaClaw',          password: env.INFO_VENTURACLAW_COM_PASSWORD || env.INFO_VENTURACLAW_PASSWORD || dwPass },
+    { id: 1779000000002,    email: 'info@designerwallcoverings.com',  role: 'admin', name: 'Info — Designer Wallcoverings', password: dwPass },
+    { id: 1779000000003,    email: 'steve@designerwallcoverings.com', role: 'admin', name: 'Steve Abrams (DW)',           password: dwPass },
+  ];
+}
+
+function parseEnv(file) {
+  if (!fs.existsSync(file)) { console.error('✗ secrets-manager .env missing at', file); process.exit(1); }
+  const env = {};
+  for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
+    const m = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
+    if (!m) continue;
+    let val = m[2];
+    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1, -1);
+    if (val) env[m[1]] = val;
+  }
+  return env;
+}
+
+function buildConnectorCreds(env) {
+  // env-keys → { connector: { field: value, ... } }
+  const out = {};
+  for (const [k, v] of Object.entries(env)) {
+    const map = ENV_KEY_TO_CONNECTOR[k];
+    if (!map) continue;
+    if (!out[map.connector]) out[map.connector] = {};
+    out[map.connector][map.field] = v;
+  }
+  const now = new Date().toISOString();
+  for (const cid of Object.keys(out)) {
+    out[cid]._imported_at = now;
+    out[cid]._import_source = 'preload-fleet';
+  }
+  return out;
+}
+
+function atomicWrite(file, data, mode = 0o600) {
+  fs.mkdirSync(path.dirname(file), { recursive: true });
+  const tmp = file + '.tmp.' + process.pid;
+  const fd = fs.openSync(tmp, 'w', mode);
+  try {
+    fs.writeSync(fd, JSON.stringify(data, null, 2));
+    fs.fsyncSync(fd);
+  } finally { fs.closeSync(fd); }
+  fs.renameSync(tmp, file);
+}
+
+function main() {
+  const WRITE = process.argv.includes('--write');
+  const env = parseEnv(SECRETS_ENV);
+  const seeds = userSeeds(env);
+  const connectorCreds = buildConnectorCreds(env);
+
+  const connectorIds = Object.keys(connectorCreds).sort();
+  const fieldCount = connectorIds.reduce((n, cid) => n + Object.keys(connectorCreds[cid]).filter(k => !k.startsWith('_')).length, 0);
+
+  console.log('\n=== Ventura-Claw fleet preload ===');
+  console.log(`  env file:         ${SECRETS_ENV} (${Object.keys(env).length} keys)`);
+  console.log(`  users to seed:    ${seeds.length}`);
+  for (const u of seeds) console.log(`     ${String(u.id).padEnd(15)}  ${u.email.padEnd(42)} ${u.role}`);
+  console.log(`  connectors live:  ${connectorIds.length} (of 67 catalog) — ${fieldCount} fields`);
+  console.log(`     ${connectorIds.join(', ')}`);
+  const unmapped = Object.keys(env).filter(k => !ENV_KEY_TO_CONNECTOR[k]);
+  console.log(`  env keys ignored: ${unmapped.length} (no connector mapping — fleet/site passwords)`);
+
+  if (!WRITE) {
+    console.log('\n→ Dry-run only. Re-run with --write to commit.\n');
+    return;
+  }
+
+  // 1. users.json — bcrypt-hash, preserve any non-seed users already on disk
+  const existingUsers = fs.existsSync(USERS_JSON) ? JSON.parse(fs.readFileSync(USERS_JSON, 'utf8')) : [];
+  const seedEmails = new Set(seeds.map(u => u.email.toLowerCase()));
+  const seedIds = new Set(seeds.map(u => String(u.id)));
+  const kept = existingUsers.filter(u => !seedEmails.has(String(u.email || '').toLowerCase()) && !seedIds.has(String(u.id)));
+  const hashed = seeds.map(u => ({
+    id: u.id,
+    email: u.email,
+    role: u.role,
+    name: u.name,
+    password: bcrypt.hashSync(u.password, 10),
+    _seeded_at: new Date().toISOString(),
+  }));
+  const nextUsers = [...hashed, ...kept];
+  atomicWrite(USERS_JSON, nextUsers, 0o644);
+  console.log(`\n✓ users.json — wrote ${hashed.length} seed users (+ ${kept.length} preserved)`);
+
+  // 2. credentials.json — back up encrypted file, write fresh plaintext for all 5 users
+  if (fs.existsSync(CREDS_JSON)) {
+    const bak = CREDS_JSON + '.bak.' + Date.now();
+    fs.copyFileSync(CREDS_JSON, bak);
+    fs.chmodSync(bak, 0o600);
+    console.log(`✓ Backed up existing credentials.json → ${path.basename(bak)}`);
+  }
+  const creds = {};
+  for (const u of seeds) {
+    creds[String(u.id)] = JSON.parse(JSON.stringify(connectorCreds)); // deep clone per user
+  }
+  atomicWrite(CREDS_JSON, creds, 0o600);
+  console.log(`✓ credentials.json — ${Object.keys(creds).length} users × ${connectorIds.length} connectors written plaintext (server boot will re-encrypt under CC_SECRET)`);
+
+  // 3. Pretty preview — show the mask format Steve asked for, no real values
+  console.log('\n--- masked preview (this is what the UI will show) ---');
+  const sampleUser = String(seeds[0].id);
+  for (const cid of connectorIds.slice(0, 4)) {
+    const fields = creds[sampleUser][cid];
+    console.log(`  ${cid}:`);
+    for (const [k, v] of Object.entries(fields)) {
+      if (k.startsWith('_')) continue;
+      const s = String(v);
+      const mask = s.length > 8 ? `${s.slice(0,4)}${'*'.repeat(8)}${s.slice(-4)}` : '••••••••';
+      console.log(`     ${k.padEnd(28)} ${mask}`);
+    }
+  }
+  console.log('\n→ Restart ventura-claw (pm2 restart ventura-claw) to load the new seed.');
+  console.log('→ On next boot, server auto-encrypts credentials.json at-rest under CC_SECRET.\n');
+}
+
+main();
diff --git a/server/server.js b/server/server.js
index c4be8c4..98477d0 100644
--- a/server/server.js
+++ b/server/server.js
@@ -322,8 +322,9 @@ function maskFields(c) {
   if (!c) return c;
   const out = {};
   for (const k of Object.keys(c)) {
+    if (k.startsWith("_")) { out[k] = c[k]; continue; } // provenance fields pass through
     const v = String(c[k] || "");
-    out[k] = v.length > 8 ? `${v.slice(0,4)}…${v.slice(-4)}` : "•••";
+    out[k] = v.length > 8 ? `${v.slice(0,4)}${"*".repeat(8)}${v.slice(-4)}` : "••••••••";
   }
   return out;
 }

← 5e81c08 snapshot — gitify backup 2026-05-19  ·  back to Ventura Claw  ·  Update Claude model IDs to claude-opus-4-8 (Opus 4.8 upgrade 7a6c4ad →