freeschema 1.0.7 → 1.0.8

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.
Files changed (2) hide show
  1. package/bin/freeschema.js +93 -94
  2. package/package.json +1 -1
package/bin/freeschema.js CHANGED
@@ -6,7 +6,6 @@ const { spawnSync } = require('child_process');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const https = require('https');
9
- const http = require('http');
10
9
  const crypto = require('crypto');
11
10
  const readline = require('readline');
12
11
 
@@ -569,8 +568,21 @@ async function cmdStart() {
569
568
  if (isPlaceholder) {
570
569
  console.log('\n─────────────────────────────────────────');
571
570
  console.log(' CLIENT_SECRET is not configured.');
572
- console.log(' Setting up admin credentials now…');
571
+ console.log(' Setting up admin credentials…');
573
572
  console.log('─────────────────────────────────────────');
573
+
574
+ // Wait for MySQL to be ready before inserting credentials
575
+ try {
576
+ const container = getMysqlContainer();
577
+ const env = readEnv();
578
+ const dbUser = env.DB_USER || env.MYSQL_USER || 'freeschema';
579
+ const dbPass = env.DB_PASSWORD || env.MYSQL_PASSWORD || 'Freeschema@123';
580
+ waitForMySQL(container, dbUser, dbPass);
581
+ } catch (e) {
582
+ console.log(' (skipping credentials setup — MySQL not running)');
583
+ return;
584
+ }
585
+
574
586
  await cmdSetupCredentials();
575
587
  }
576
588
  }
@@ -716,126 +728,113 @@ function cmdDbRestore() {
716
728
 
717
729
  // ── setup-credentials ─────────────────────────────────────────────────────────
718
730
 
719
- function httpPost(url, body) {
720
- return new Promise((resolve, reject) => {
721
- const parsed = new URL(url);
722
- const payload = JSON.stringify(body);
723
- const opts = {
724
- hostname: parsed.hostname,
725
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
726
- path: parsed.pathname,
727
- method: 'POST',
728
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
729
- };
730
- const lib = parsed.protocol === 'https:' ? https : http;
731
- const req = lib.request(opts, res => {
732
- let data = '';
733
- res.on('data', chunk => { data += chunk; });
734
- res.on('end', () => {
735
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
736
- catch { resolve({ status: res.statusCode, body: data }); }
737
- });
738
- });
739
- req.on('error', reject);
740
- req.write(payload);
741
- req.end();
742
- });
731
+ function hashClientSecret(plainSecret) {
732
+ // Matches C# ClientSecretHasher: PBKDF2-HMAC-SHA256, 100000 iterations, 16-byte salt, 32-byte output
733
+ // Stored format: "{iterations}.{base64_salt}.{base64_hash}"
734
+ const iterations = 100_000;
735
+ const salt = crypto.randomBytes(16);
736
+ const hash = crypto.pbkdf2Sync(plainSecret, salt, iterations, 32, 'sha256');
737
+ return `${iterations}.${salt.toString('base64')}.${hash.toString('base64')}`;
743
738
  }
744
739
 
745
- function httpPostWithToken(url, token) {
746
- return new Promise((resolve, reject) => {
747
- const parsed = new URL(url);
748
- const opts = {
749
- hostname: parsed.hostname,
750
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
751
- path: parsed.pathname,
752
- method: 'POST',
753
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Length': 0 },
754
- };
755
- const lib = parsed.protocol === 'https:' ? https : http;
756
- const req = lib.request(opts, res => {
757
- let data = '';
758
- res.on('data', chunk => { data += chunk; });
759
- res.on('end', () => {
760
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
761
- catch { resolve({ status: res.statusCode, body: data }); }
762
- });
763
- });
764
- req.on('error', reject);
765
- req.end();
766
- });
740
+ function runSql(container, dbName, sql) {
741
+ const env = readEnv();
742
+ const dbPass = env.MYSQL_ROOT_PASSWORD || ('Admin@' + (env.MYSQL_PASSWORD || 'Freeschema@123'));
743
+ const result = spawnSync(
744
+ 'docker',
745
+ ['exec', '-i', container, 'mysql', '-uroot', `-p${dbPass}`, dbName],
746
+ { input: sql, stdio: ['pipe', 'pipe', 'pipe'] }
747
+ );
748
+ if (result.error) throw new Error(`docker exec failed: ${result.error.message}`);
749
+ if (result.status !== 0) throw new Error((result.stderr || '').toString().trim());
750
+ return (result.stdout || '').toString().trim();
767
751
  }
768
752
 
769
753
  async function cmdSetupCredentials() {
770
754
  const envPath = path.join(process.cwd(), '.env');
771
755
  if (!fs.existsSync(envPath)) die('.env not found. Run `freeschema init` first.');
772
756
 
773
- const env = readEnv();
774
- const wicoPort = env.WICO_PORT || '7071';
775
- const baseUrl = `http://localhost:${wicoPort}/api`;
757
+ const env = readEnv();
758
+ const dbName = env.DB_DATABASE || env.MYSQL_DATABASE || 'freeschema_db';
776
759
 
777
- console.log(`\nSetting up CLIENT_ID and CLIENT_SECRET`);
778
- console.log(`Using WICO server at ${baseUrl}\n`);
760
+ console.log('\nSetting up CLIENT_ID and CLIENT_SECRET');
779
761
 
780
- // Step 1: login
781
- const email = await prompt('Admin email');
782
- const password = await promptSecret('Admin password');
783
- if (!email || !password) die('Email and password are required.');
762
+ // Prompt for entity concept ID
763
+ const entityId = await prompt('Admin entity concept ID', env.CLIENT_ID || '100000016');
764
+ if (!entityId) die('Entity ID is required.');
784
765
 
785
- process.stdout.write('\n[1/3] Logging in… ');
786
- let loginRes;
787
- try {
788
- loginRes = await httpPost(`${baseUrl}/auth/login`, { email, password, application: 'boomconsole' });
789
- } catch (e) {
790
- die(`Could not reach WICO server at ${baseUrl}/auth/login\nIs FreeSchema running? Try: freeschema start`);
791
- }
792
-
793
- if (loginRes.status !== 200 || !loginRes.body) {
794
- die(`Login failed (HTTP ${loginRes.status}): ${JSON.stringify(loginRes.body)}`);
795
- }
766
+ // Step 1: generate secret + hash it locally — no API call needed
767
+ process.stdout.write('[1/3] Generating secret… ');
768
+ const plainSecret = crypto.randomBytes(32).toString('hex'); // 64-char hex
769
+ const hashedSecret = hashClientSecret(plainSecret);
770
+ console.log('done');
796
771
 
797
- const token = loginRes.body.token || loginRes.body.access_token || (loginRes.body.data && loginRes.body.data.token);
798
- if (!token) die(`No token in login response: ${JSON.stringify(loginRes.body)}`);
772
+ // Step 2: insert hash directly into the database
773
+ process.stdout.write('[2/3] Writing to database… ');
774
+ const container = getMysqlContainer();
799
775
 
800
- // Decode entity concept ID from JWT upn claim
801
- let entityId;
802
- try {
803
- const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
804
- entityId = payload.upn || payload.sub;
805
- } catch {
806
- die('Could not decode JWT token.');
807
- }
808
- if (!entityId) die('Could not determine entity ID from JWT.');
809
- console.log(`done (entity ${entityId})`);
776
+ const sql = `
777
+ SET @entity_id = ${parseInt(entityId, 10)};
778
+
779
+ -- Look up the type concept for 'the_client_secret'
780
+ SET @type_id = (
781
+ SELECT id FROM the_concepts
782
+ WHERE character_value = 'the_client_secret'
783
+ ORDER BY id ASC LIMIT 1
784
+ );
785
+
786
+ -- Insert the secret hash as a new concept
787
+ INSERT INTO the_concepts (
788
+ user_id, category_id, category_user_id, type_id, type_user_id,
789
+ character_value, security_id, security_user_id, access_id, access_user_id,
790
+ session_information_id, session_information_user_id
791
+ ) VALUES (
792
+ 999, @type_id, 999, @type_id, 999,
793
+ '${hashedSecret}', 4, 999, 4, 999, 999, 999
794
+ );
795
+
796
+ SET @secret_concept_id = LAST_INSERT_ID();
797
+
798
+ -- Look up the connection type for 'the_entity_s_client_secret'
799
+ SET @conn_type_id = (
800
+ SELECT id FROM the_concepts
801
+ WHERE character_value = 'the_entity_s_client_secret'
802
+ ORDER BY id ASC LIMIT 1
803
+ );
804
+
805
+ -- Link entity → secret concept
806
+ INSERT INTO the_connections (
807
+ user_id, of_the_concepts_id, of_the_concepts_user_id,
808
+ type_id, type_user_id, order_id, order_user_id,
809
+ to_the_concepts_id, to_the_concepts_user_id,
810
+ security_id, security_user_id, access_id, access_user_id,
811
+ session_information_id, session_information_user_id
812
+ ) VALUES (
813
+ 999, @entity_id, 999,
814
+ @conn_type_id, 999, 999, 999,
815
+ @secret_concept_id, 999,
816
+ 4, 999, 4, 999, 999, 999
817
+ );
818
+ `;
810
819
 
811
- // Step 2: generate client-secret
812
- process.stdout.write('[2/3] Generating client-secret… ');
813
- let secretRes;
814
820
  try {
815
- secretRes = await httpPostWithToken(`${baseUrl}/client-secret`, token);
821
+ runSql(container, dbName, sql);
816
822
  } catch (e) {
817
- die(`Request to /api/client-secret failed: ${e.message}`);
823
+ die(`Database insert failed: ${e.message}`);
818
824
  }
819
-
820
- if (secretRes.status !== 200 || !secretRes.body) {
821
- die(`client-secret failed (HTTP ${secretRes.status}): ${JSON.stringify(secretRes.body)}`);
822
- }
823
-
824
- const secret = secretRes.body.message || secretRes.body.data || secretRes.body;
825
- if (!secret || typeof secret !== 'string') die(`Empty secret in response: ${JSON.stringify(secretRes.body)}`);
826
825
  console.log('done');
827
826
 
828
827
  // Step 3: update .env
829
828
  process.stdout.write('[3/3] Updating .env… ');
830
829
  setEnvVar(envPath, 'CLIENT_ID', String(entityId));
831
- setEnvVar(envPath, 'CLIENT_SECRET', secret);
830
+ setEnvVar(envPath, 'CLIENT_SECRET', plainSecret);
832
831
  console.log('done');
833
832
 
834
833
  console.log(`
835
834
  ─────────────────────────────────────────
836
835
  Credentials saved to .env:
837
836
  CLIENT_ID = ${entityId}
838
- CLIENT_SECRET = ${secret}
837
+ CLIENT_SECRET = ${plainSecret}
839
838
  ─────────────────────────────────────────
840
839
 
841
840
  Restart the node-server to apply:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freeschema",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "FreeSchema — self-hosted deployment CLI",
5
5
  "keywords": [
6
6
  "freeschema",