@novaluna/onboard 1.0.0
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/dist/cli.d.ts +2 -0
- package/dist/cli.js +210 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +58 -0
- package/dist/db.d.ts +8 -0
- package/dist/db.js +38 -0
- package/dist/gateway.d.ts +2 -0
- package/dist/gateway.js +34 -0
- package/dist/keycloak.d.ts +3 -0
- package/dist/keycloak.js +78 -0
- package/package.json +46 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
/**
|
|
41
|
+
* novaluna-onboard CLI
|
|
42
|
+
*
|
|
43
|
+
* Usage (interactive):
|
|
44
|
+
* npx @novaluna/onboard
|
|
45
|
+
*
|
|
46
|
+
* Usage (non-interactive / CI):
|
|
47
|
+
* npx @novaluna/onboard provision \
|
|
48
|
+
* --org "Acme Capital" \
|
|
49
|
+
* --email admin@acmecapital.com \
|
|
50
|
+
* --realm acme-capital
|
|
51
|
+
*/
|
|
52
|
+
const commander_1 = require("commander");
|
|
53
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
54
|
+
const ora_1 = __importDefault(require("ora"));
|
|
55
|
+
const promises_1 = require("readline/promises");
|
|
56
|
+
const db = __importStar(require("./db"));
|
|
57
|
+
const keycloak = __importStar(require("./keycloak"));
|
|
58
|
+
const gateway = __importStar(require("./gateway"));
|
|
59
|
+
const db_1 = require("./db");
|
|
60
|
+
const WORKBENCH_URL = 'https://workbench.novaluna.ai';
|
|
61
|
+
const program = new commander_1.Command()
|
|
62
|
+
.name('novaluna-onboard')
|
|
63
|
+
.description('Provision a new Novaluna client org')
|
|
64
|
+
.version('1.0.0');
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// provision command — full onboarding flow
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
program
|
|
69
|
+
.command('provision', { isDefault: true })
|
|
70
|
+
.description('Interactively provision a new client org (default command)')
|
|
71
|
+
.option('--org <name>', 'Organisation name')
|
|
72
|
+
.option('--email <email>', 'Admin email')
|
|
73
|
+
.option('--realm <id>', 'Keycloak realm ID (lowercase, no spaces)')
|
|
74
|
+
.option('--token-name <n>', 'Agent token name', 'default')
|
|
75
|
+
.action(async (opts) => {
|
|
76
|
+
console.log(chalk_1.default.bold('\nNovaluna Client Onboarding'));
|
|
77
|
+
console.log(chalk_1.default.dim('─'.repeat(40)));
|
|
78
|
+
// Gather inputs interactively if not provided as flags
|
|
79
|
+
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
80
|
+
const ask = (q) => rl.question(chalk_1.default.cyan('? ') + q + ' ');
|
|
81
|
+
const orgName = opts.org ?? (await ask('Organisation name:')).trim();
|
|
82
|
+
const email = opts.email ?? (await ask('Admin email:')).trim();
|
|
83
|
+
const defaultRealm = orgName.toLowerCase().replace(/\s+/g, '-');
|
|
84
|
+
const realmInput = opts.realm ?? (await ask(`Keycloak realm ID [${defaultRealm}]: `)).trim();
|
|
85
|
+
const realmId = realmInput || defaultRealm;
|
|
86
|
+
rl.close();
|
|
87
|
+
const tokenName = opts.tokenName;
|
|
88
|
+
let orgId;
|
|
89
|
+
// 1. Create org in MySQL (idempotent)
|
|
90
|
+
{
|
|
91
|
+
const spinner = (0, ora_1.default)('Creating org in database...').start();
|
|
92
|
+
try {
|
|
93
|
+
const existing = await db.getOrgByName(orgName);
|
|
94
|
+
if (existing) {
|
|
95
|
+
orgId = existing.id;
|
|
96
|
+
spinner.info(`Org already exists (id=${orgId})`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
orgId = await db.createOrg(orgName, email);
|
|
100
|
+
spinner.succeed(`Org created (id=${orgId})`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
spinner.fail(`DB error: ${err.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2. Create Keycloak realm (idempotent)
|
|
109
|
+
{
|
|
110
|
+
const spinner = (0, ora_1.default)('Provisioning Keycloak realm...').start();
|
|
111
|
+
try {
|
|
112
|
+
if (await keycloak.realmExists(realmId)) {
|
|
113
|
+
spinner.info(`Realm already exists: ${realmId}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
await keycloak.createRealm(realmId, orgName);
|
|
117
|
+
await keycloak.createClient(realmId, 'novaluna-workbench', WORKBENCH_URL);
|
|
118
|
+
spinner.succeed(`Realm created: ${realmId}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
spinner.fail(`Keycloak error: ${err.message}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 3. Generate agent registration token
|
|
127
|
+
let agentToken;
|
|
128
|
+
{
|
|
129
|
+
const spinner = (0, ora_1.default)('Generating agent token...').start();
|
|
130
|
+
try {
|
|
131
|
+
agentToken = await gateway.createAgentToken(orgId, tokenName, email);
|
|
132
|
+
spinner.succeed('Agent token generated');
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
spinner.fail(`Gateway error: ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await (0, db_1.close)();
|
|
140
|
+
// 4. Print summary
|
|
141
|
+
console.log('\n' + chalk_1.default.bold.green('✓ Onboarding complete'));
|
|
142
|
+
console.log(chalk_1.default.dim('─'.repeat(40)));
|
|
143
|
+
console.log(` Organisation ${chalk_1.default.cyan(orgName)} (id=${orgId})`);
|
|
144
|
+
console.log(` Keycloak realm ${chalk_1.default.cyan(realmId)}`);
|
|
145
|
+
console.log(` Admin email ${chalk_1.default.cyan(email)}`);
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(chalk_1.default.bold('Agent install command for your team:'));
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(chalk_1.default.bgBlack.white(` npx @novaluna/agent start --token ${agentToken}`));
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk_1.default.dim(' Share this token securely with the client.'));
|
|
152
|
+
console.log(chalk_1.default.dim(` Workbench: ${WORKBENCH_URL}\n`));
|
|
153
|
+
});
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// token command — generate an additional token for an existing org
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
program
|
|
158
|
+
.command('token')
|
|
159
|
+
.description('Generate an additional agent token for an existing org')
|
|
160
|
+
.requiredOption('--org <name>', 'Organisation name')
|
|
161
|
+
.option('--name <label>', 'Token label', 'additional')
|
|
162
|
+
.option('--by <email>', 'Created by', 'admin')
|
|
163
|
+
.action(async (opts) => {
|
|
164
|
+
const spinner = (0, ora_1.default)(`Looking up org: ${opts.org}`).start();
|
|
165
|
+
const org = await db.getOrgByName(opts.org);
|
|
166
|
+
if (!org) {
|
|
167
|
+
spinner.fail(`Org not found: ${opts.org}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
spinner.succeed(`Org found (id=${org.id})`);
|
|
171
|
+
const tokenSpinner = (0, ora_1.default)('Generating token...').start();
|
|
172
|
+
try {
|
|
173
|
+
const raw = await gateway.createAgentToken(org.id, opts.name, opts.by);
|
|
174
|
+
tokenSpinner.succeed('Token generated');
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk_1.default.bgBlack.white(` npx @novaluna/agent start --token ${raw}`));
|
|
177
|
+
console.log('');
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
tokenSpinner.fail(err.message);
|
|
181
|
+
}
|
|
182
|
+
await (0, db_1.close)();
|
|
183
|
+
});
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// status command — show connected agents for an org
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
program
|
|
188
|
+
.command('status')
|
|
189
|
+
.description('Show connected agents for an org')
|
|
190
|
+
.requiredOption('--org <name>', 'Organisation name')
|
|
191
|
+
.action(async (opts) => {
|
|
192
|
+
const org = await db.getOrgByName(opts.org);
|
|
193
|
+
if (!org) {
|
|
194
|
+
console.error(chalk_1.default.red(`Org not found: ${opts.org}`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
const agents = await gateway.listAgents(org.id);
|
|
198
|
+
if (agents.length === 0) {
|
|
199
|
+
console.log(chalk_1.default.yellow('No agents connected.'));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
console.log(chalk_1.default.bold(`\nConnected agents for ${opts.org}:\n`));
|
|
203
|
+
for (const a of agents) {
|
|
204
|
+
const dot = a.connected ? chalk_1.default.green('●') : chalk_1.default.grey('○');
|
|
205
|
+
console.log(` ${dot} ${a.agent_id} MCPs: ${(a.mcp_ids ?? []).join(', ') || 'none'}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await (0, db_1.close)();
|
|
209
|
+
});
|
|
210
|
+
program.parse(process.argv);
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const config: {
|
|
2
|
+
db: {
|
|
3
|
+
host: string;
|
|
4
|
+
port: number;
|
|
5
|
+
database: string;
|
|
6
|
+
user: string;
|
|
7
|
+
password: string;
|
|
8
|
+
};
|
|
9
|
+
keycloak: {
|
|
10
|
+
url: string;
|
|
11
|
+
realm: string;
|
|
12
|
+
adminUser: string;
|
|
13
|
+
adminPass: string;
|
|
14
|
+
clientId: string;
|
|
15
|
+
};
|
|
16
|
+
gateway: {
|
|
17
|
+
url: string;
|
|
18
|
+
internalSecret: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.config = void 0;
|
|
37
|
+
const dotenv = __importStar(require("dotenv"));
|
|
38
|
+
dotenv.config();
|
|
39
|
+
exports.config = {
|
|
40
|
+
db: {
|
|
41
|
+
host: process.env.DB_HOST ?? 'localhost',
|
|
42
|
+
port: Number(process.env.DB_PORT ?? 3306),
|
|
43
|
+
database: process.env.DB_NAME ?? 'novaluna',
|
|
44
|
+
user: process.env.DB_USER ?? 'novaluna',
|
|
45
|
+
password: process.env.DB_PASSWORD ?? '',
|
|
46
|
+
},
|
|
47
|
+
keycloak: {
|
|
48
|
+
url: process.env.KEYCLOAK_URL ?? 'https://auth.novaluna.ai',
|
|
49
|
+
realm: process.env.KEYCLOAK_ADMIN_REALM ?? 'master',
|
|
50
|
+
adminUser: process.env.KEYCLOAK_ADMIN_USER ?? 'admin',
|
|
51
|
+
adminPass: process.env.KEYCLOAK_ADMIN_PASS ?? '',
|
|
52
|
+
clientId: process.env.KEYCLOAK_ADMIN_CLIENT ?? 'admin-cli',
|
|
53
|
+
},
|
|
54
|
+
gateway: {
|
|
55
|
+
url: process.env.GATEWAY_URL ?? 'https://agents.novaluna.ai',
|
|
56
|
+
internalSecret: process.env.GATEWAY_INTERNAL_SECRET ?? '',
|
|
57
|
+
},
|
|
58
|
+
};
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import mysql from 'mysql2/promise';
|
|
2
|
+
export declare function getConn(): Promise<mysql.Connection>;
|
|
3
|
+
export declare function close(): Promise<void>;
|
|
4
|
+
export declare function orgExists(name: string): Promise<boolean>;
|
|
5
|
+
export declare function createOrg(name: string, adminEmail: string): Promise<number>;
|
|
6
|
+
export declare function getOrgByName(name: string): Promise<{
|
|
7
|
+
id: number;
|
|
8
|
+
} | null>;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getConn = getConn;
|
|
7
|
+
exports.close = close;
|
|
8
|
+
exports.orgExists = orgExists;
|
|
9
|
+
exports.createOrg = createOrg;
|
|
10
|
+
exports.getOrgByName = getOrgByName;
|
|
11
|
+
const promise_1 = __importDefault(require("mysql2/promise"));
|
|
12
|
+
const config_1 = require("./config");
|
|
13
|
+
let _conn = null;
|
|
14
|
+
async function getConn() {
|
|
15
|
+
if (!_conn) {
|
|
16
|
+
_conn = await promise_1.default.createConnection(config_1.config.db);
|
|
17
|
+
}
|
|
18
|
+
return _conn;
|
|
19
|
+
}
|
|
20
|
+
async function close() {
|
|
21
|
+
await _conn?.end();
|
|
22
|
+
}
|
|
23
|
+
async function orgExists(name) {
|
|
24
|
+
const conn = await getConn();
|
|
25
|
+
// organisations are stored in the novaluna core DB — adjust table name if different
|
|
26
|
+
const [rows] = await conn.query('SELECT id FROM organisation WHERE name = ? LIMIT 1', [name]);
|
|
27
|
+
return rows.length > 0;
|
|
28
|
+
}
|
|
29
|
+
async function createOrg(name, adminEmail) {
|
|
30
|
+
const conn = await getConn();
|
|
31
|
+
const [result] = await conn.query('INSERT INTO organisation (name, admin_email, created_at) VALUES (?, ?, NOW())', [name, adminEmail]);
|
|
32
|
+
return result.insertId;
|
|
33
|
+
}
|
|
34
|
+
async function getOrgByName(name) {
|
|
35
|
+
const conn = await getConn();
|
|
36
|
+
const [rows] = await conn.query('SELECT id FROM organisation WHERE name = ? LIMIT 1', [name]);
|
|
37
|
+
return rows.length ? rows[0] : null;
|
|
38
|
+
}
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAgentToken = createAgentToken;
|
|
4
|
+
exports.listAgents = listAgents;
|
|
5
|
+
/**
|
|
6
|
+
* Gateway API client — calls agent-gateway REST endpoints.
|
|
7
|
+
*/
|
|
8
|
+
const config_1 = require("./config");
|
|
9
|
+
function headers() {
|
|
10
|
+
return {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'x-internal-secret': config_1.config.gateway.internalSecret,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
async function createAgentToken(orgId, name, createdBy) {
|
|
16
|
+
const res = await fetch(`${config_1.config.gateway.url}/internal/tokens`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: headers(),
|
|
19
|
+
body: JSON.stringify({ org_id: orgId, name, created_by: createdBy }),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok)
|
|
22
|
+
throw new Error(`Token creation failed: ${res.status} ${await res.text()}`);
|
|
23
|
+
const data = (await res.json());
|
|
24
|
+
return data.token;
|
|
25
|
+
}
|
|
26
|
+
async function listAgents(orgId) {
|
|
27
|
+
const res = await fetch(`${config_1.config.gateway.url}/internal/orgs/${orgId}/agents`, {
|
|
28
|
+
headers: headers(),
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
throw new Error(`List agents failed: ${res.status}`);
|
|
32
|
+
const data = (await res.json());
|
|
33
|
+
return data.agents;
|
|
34
|
+
}
|
package/dist/keycloak.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.realmExists = realmExists;
|
|
4
|
+
exports.createRealm = createRealm;
|
|
5
|
+
exports.createClient = createClient;
|
|
6
|
+
/**
|
|
7
|
+
* Keycloak admin operations via REST API.
|
|
8
|
+
* Creates a new realm + client for each onboarded org.
|
|
9
|
+
*/
|
|
10
|
+
const config_1 = require("./config");
|
|
11
|
+
let _adminToken = null;
|
|
12
|
+
async function getAdminToken() {
|
|
13
|
+
if (_adminToken)
|
|
14
|
+
return _adminToken;
|
|
15
|
+
const { url, realm, adminUser, adminPass, clientId } = config_1.config.keycloak;
|
|
16
|
+
const res = await fetch(`${url}/realms/${realm}/protocol/openid-connect/token`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
19
|
+
body: new URLSearchParams({
|
|
20
|
+
grant_type: 'password',
|
|
21
|
+
client_id: clientId,
|
|
22
|
+
username: adminUser,
|
|
23
|
+
password: adminPass,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(`Keycloak admin login failed: ${res.status} ${await res.text()}`);
|
|
28
|
+
const data = (await res.json());
|
|
29
|
+
_adminToken = data.access_token;
|
|
30
|
+
return _adminToken;
|
|
31
|
+
}
|
|
32
|
+
async function adminFetch(path, opts = {}) {
|
|
33
|
+
const token = await getAdminToken();
|
|
34
|
+
return fetch(`${config_1.config.keycloak.url}${path}`, {
|
|
35
|
+
...opts,
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: `Bearer ${token}`,
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...(opts.headers ?? {}),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function realmExists(realmId) {
|
|
44
|
+
const res = await adminFetch(`/admin/realms/${realmId}`);
|
|
45
|
+
return res.ok;
|
|
46
|
+
}
|
|
47
|
+
async function createRealm(realmId, displayName) {
|
|
48
|
+
const res = await adminFetch('/admin/realms', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
realm: realmId,
|
|
52
|
+
displayName,
|
|
53
|
+
enabled: true,
|
|
54
|
+
sslRequired: 'external',
|
|
55
|
+
registrationAllowed: false,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok && res.status !== 409) {
|
|
59
|
+
throw new Error(`Create realm failed: ${res.status} ${await res.text()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function createClient(realmId, clientId, rootUrl) {
|
|
63
|
+
const res = await adminFetch(`/admin/realms/${realmId}/clients`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
clientId,
|
|
67
|
+
rootUrl,
|
|
68
|
+
redirectUris: [`${rootUrl}/*`],
|
|
69
|
+
webOrigins: [rootUrl],
|
|
70
|
+
publicClient: true,
|
|
71
|
+
standardFlowEnabled: true,
|
|
72
|
+
directAccessGrantsEnabled: false,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok && res.status !== 409) {
|
|
76
|
+
throw new Error(`Create client failed: ${res.status} ${await res.text()}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@novaluna/onboard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Novaluna client onboarding CLI — provisions orgs, Keycloak realms, and agent tokens",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"novaluna-onboard": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "ts-node src/cli.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"novaluna",
|
|
23
|
+
"onboarding",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/novaluna-ai/novaluna-agent.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://workbench.novaluna.ai",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.1.0",
|
|
34
|
+
"chalk": "^4.1.2",
|
|
35
|
+
"ora": "^5.4.1",
|
|
36
|
+
"mysql2": "^3.11.0",
|
|
37
|
+
"dotenv": "^16.4.5"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"typescript": "^5.6.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|