← 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
A scripts/preload-fleet.jsM server/server.js
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 →