Skip to content

Commit d20dcb0

Browse files
rafaelscostaclaude
andcommitted
feat(pro): EPIC-PRO-17 seat hygiene client and CLI [STORY-17.1][STORY-17.5][STORY-17.6]
- Bump @aiox-squads/pro submodule with persisted machine id and telemetry - Unify machine-id barrel across install wizard, activate, and artifact broker - Add aiox pro seats list|release self-service commands and license tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e840b5 commit d20dcb0

7 files changed

Lines changed: 336 additions & 18 deletions

File tree

.aiox-core/cli/commands/pro/index.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const path = require('path');
2323
const fs = require('fs');
2424
const readline = require('readline');
2525
const { createBuyerCommand } = require('./buyer');
26+
const { createSeatsCommand } = require('./seats');
2627
const PRO_PACKAGE = '@aiox-squads/pro';
2728

2829
// BUG-6 fix (INS-1): Dynamic licensePath resolution
@@ -75,9 +76,8 @@ function loadLicenseModules() {
7576
setPendingDeactivation,
7677
clearPendingDeactivation,
7778
} = require(path.join(licensePath, 'license-cache'));
78-
const { generateMachineId, maskKey, validateKeyFormat } = require(
79-
path.join(licensePath, 'license-crypto')
80-
);
79+
const { generateMachineId } = require(path.join(licensePath, 'machine-id'));
80+
const { maskKey, validateKeyFormat } = require(path.join(licensePath, 'license-crypto'));
8181
const { ProFeatureError, LicenseActivationError } = require(path.join(licensePath, 'errors'));
8282

8383
return {
@@ -775,6 +775,9 @@ function createProCommand() {
775775
// aiox pro buyer — Cohort admin operations (Story 123.8)
776776
proCmd.addCommand(createBuyerCommand());
777777

778+
// aiox pro seats — Self-service seat management (EPIC-PRO-17)
779+
proCmd.addCommand(createSeatsCommand());
780+
778781
return proCmd;
779782
}
780783

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/**
2+
* Pro Seats Self-Service CLI (EPIC-PRO-17 / STORY-PRO-17.5)
3+
*
4+
* aiox pro seats list [--email E] [--password P] [--token T]
5+
* aiox pro seats release <activationId> [--email E] [--password P] [--token T]
6+
*
7+
* @module cli/commands/pro/seats
8+
*/
9+
10+
'use strict';
11+
12+
const { Command } = require('commander');
13+
const fs = require('fs');
14+
const path = require('path');
15+
16+
const PRO_PACKAGE = '@aiox-squads/pro';
17+
18+
function resolveLicensePath() {
19+
const relativePath = path.resolve(__dirname, '..', '..', '..', '..', 'pro', 'license');
20+
if (fs.existsSync(relativePath)) {
21+
return relativePath;
22+
}
23+
24+
try {
25+
const proPkg = require.resolve(`${PRO_PACKAGE}/package.json`);
26+
const npmPath = path.join(path.dirname(proPkg), 'license');
27+
if (fs.existsSync(npmPath)) {
28+
return npmPath;
29+
}
30+
} catch {
31+
// package not installed
32+
}
33+
34+
const cwdPath = path.join(process.cwd(), 'node_modules', '@aiox-squads', 'pro', 'license');
35+
if (fs.existsSync(cwdPath)) {
36+
return cwdPath;
37+
}
38+
39+
return relativePath;
40+
}
41+
42+
const licensePath = resolveLicensePath();
43+
44+
function loadModules() {
45+
try {
46+
const { licenseApi } = require(path.join(licensePath, 'license-api'));
47+
const { generateMachineId } = require(path.join(licensePath, 'machine-id'));
48+
const { AuthError } = require(path.join(licensePath, 'errors'));
49+
return { licenseApi, generateMachineId, AuthError };
50+
} catch (error) {
51+
console.error('AIOX Pro license module not available.');
52+
console.error('Install AIOX Pro: aiox pro setup');
53+
console.error(error.message);
54+
process.exit(1);
55+
}
56+
}
57+
58+
async function resolveAccessToken(options, AuthError) {
59+
const { licenseApi } = loadModules();
60+
61+
if (options.token) {
62+
return options.token;
63+
}
64+
65+
const email = options.email || process.env.AIOX_PRO_EMAIL;
66+
const password = options.password || process.env.AIOX_PRO_PASSWORD;
67+
68+
if (!email || !password) {
69+
console.error('Authentication required.');
70+
console.error('Use --email and --password, --token, or AIOX_PRO_EMAIL/AIOX_PRO_PASSWORD.');
71+
process.exit(1);
72+
}
73+
74+
try {
75+
const login = await licenseApi.login(email, password);
76+
return login.sessionToken;
77+
} catch (error) {
78+
const message = error instanceof AuthError ? error.message : error.message;
79+
console.error(`Login failed: ${message}`);
80+
process.exit(1);
81+
}
82+
}
83+
84+
function formatDate(dateStr) {
85+
if (!dateStr) return '—';
86+
return new Date(dateStr).toLocaleString('pt-BR');
87+
}
88+
89+
async function listSeatsAction(options) {
90+
const { licenseApi, generateMachineId, AuthError } = loadModules();
91+
const accessToken = await resolveAccessToken(options, AuthError);
92+
const machineId = generateMachineId();
93+
94+
try {
95+
const result = await licenseApi.listSeats(accessToken, machineId);
96+
97+
console.log('\nAIOX Pro — Seats\n');
98+
console.log(` Used: ${result.summary.used}/${result.summary.max}`);
99+
console.log(` Available: ${result.summary.available}`);
100+
console.log('');
101+
102+
if (!result.seats || result.seats.length === 0) {
103+
console.log(' No active seats.');
104+
console.log('');
105+
return;
106+
}
107+
108+
for (const seat of result.seats) {
109+
const current = seat.isCurrentMachine ? ' (esta máquina)' : '';
110+
console.log(` • ${seat.machineIdMasked}${current}`);
111+
console.log(` ID: ${seat.id}`);
112+
if (seat.machineName) {
113+
console.log(` Nome: ${seat.machineName}`);
114+
}
115+
if (seat.aiosVersion) {
116+
console.log(` Versão: ${seat.aiosVersion}`);
117+
}
118+
console.log(` Ativado: ${formatDate(seat.activatedAt)}`);
119+
console.log(` Validado: ${formatDate(seat.lastValidatedAt)}`);
120+
console.log('');
121+
}
122+
} catch (error) {
123+
console.error(`\nFailed to list seats: ${error.message}`);
124+
if (error.code) {
125+
console.error(`Error code: ${error.code}`);
126+
}
127+
process.exit(1);
128+
}
129+
}
130+
131+
async function releaseSeatAction(activationId, options) {
132+
const { licenseApi, generateMachineId, AuthError } = loadModules();
133+
134+
if (!activationId) {
135+
console.error('Usage: aiox pro seats release <activationId>');
136+
process.exit(1);
137+
}
138+
139+
const accessToken = await resolveAccessToken(options, AuthError);
140+
const machineId = generateMachineId();
141+
142+
try {
143+
const result = await licenseApi.releaseSeat(accessToken, activationId, machineId);
144+
145+
console.log('\nSeat released successfully.\n');
146+
console.log(` Released: ${result.releasedActivationId || activationId}`);
147+
console.log(` Available: ${result.summary.available}/${result.summary.max}`);
148+
console.log('');
149+
} catch (error) {
150+
console.error(`\nFailed to release seat: ${error.message}`);
151+
if (error.code) {
152+
console.error(`Error code: ${error.code}`);
153+
}
154+
process.exit(1);
155+
}
156+
}
157+
158+
function createSeatsCommand() {
159+
const seatsCmd = new Command('seats').description('List and release Pro seats (self-service)');
160+
161+
const authOptions = (cmd) => {
162+
cmd
163+
.option('--email <email>', 'Account email')
164+
.option('--password <password>', 'Account password')
165+
.option('--token <token>', 'Supabase access token');
166+
return cmd;
167+
};
168+
169+
authOptions(seatsCmd.command('list').description('List active seats'))
170+
.action(listSeatsAction);
171+
172+
authOptions(
173+
seatsCmd
174+
.command('release')
175+
.description('Release a remote seat by activation id')
176+
.argument('<activationId>', 'Seat activation UUID'),
177+
).action(releaseSeatAction);
178+
179+
return seatsCmd;
180+
}
181+
182+
module.exports = {
183+
createSeatsCommand,
184+
};

.aiox-core/install-manifest.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
# - File types for categorization
99
#
1010
version: 5.2.7
11-
generated_at: "2026-05-31T23:32:34.066Z"
11+
generated_at: "2026-07-08T18:25:48.862Z"
1212
generator: scripts/generate-install-manifest.js
13-
file_count: 1129
13+
file_count: 1130
1414
files:
1515
- path: cli/commands/config/index.js
1616
hash: sha256:25c4b9bf4e0241abf7754b55153f49f1a214f1fb5fe904a576675634cb7b3da9
@@ -105,9 +105,13 @@ files:
105105
type: cli
106106
size: 11845
107107
- path: cli/commands/pro/index.js
108-
hash: sha256:9646c62ad5b495002a66e380246306d42984d704653f69faa760278f7de05bc8
108+
hash: sha256:fa5906bb654591a24c9f59714f2018eb649d46acc593f6b6da101a703b855c7e
109109
type: cli
110-
size: 24584
110+
size: 24796
111+
- path: cli/commands/pro/seats.js
112+
hash: sha256:0762f8e976a4f1be8d34f4ab2a6f74b9236b14102d07e3f86226bc55a6a6fc75
113+
type: cli
114+
size: 5485
111115
- path: cli/commands/qa/index.js
112116
hash: sha256:3a9e30419a66e56781f9b5dcddc8f4dd0ed24dabf8fe8c3005cd26f5cb02558f
113117
type: cli

packages/installer/src/wizard/pro-setup.js

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,13 @@ class InlineLicenseClient {
270270
return this._request(
271271
'POST',
272272
'/api/v1/auth/activate-pro',
273-
{
273+
withMachineIdSource({
274274
accessToken: token,
275275
machineId,
276276
version,
277277
aioxCoreVersion: version,
278-
},
278+
aiosCoreVersion: version,
279+
}),
279280
{
280281
// Preserve legacy compatibility with older server deployments.
281282
Authorization: `Bearer ${token}`,
@@ -293,7 +294,7 @@ class InlineLicenseClient {
293294
* @returns {Promise<Object>} Artifact descriptor with artifactUrl, sha256, sizeBytes
294295
*/
295296
async getProArtifactUrl(token, request) {
296-
return this._request('POST', '/api/v1/pro/artifact-url', request, {
297+
return this._request('POST', '/api/v1/pro/artifact-url', withMachineIdSource(request), {
297298
Authorization: `Bearer ${token}`,
298299
});
299300
}
@@ -306,13 +307,17 @@ class InlineLicenseClient {
306307
* @returns {Promise<Object>} Activation result
307308
*/
308309
async activate(licenseKey, machineId, version) {
309-
return this._request('POST', '/api/v1/license/activate', {
310-
key: licenseKey,
311-
machineId,
312-
aioxCoreVersion: version,
313-
aiosCoreVersion: version,
314-
version,
315-
});
310+
return this._request(
311+
'POST',
312+
'/api/v1/license/activate',
313+
withMachineIdSource({
314+
key: licenseKey,
315+
machineId,
316+
aioxCoreVersion: version,
317+
aiosCoreVersion: version,
318+
version,
319+
}),
320+
);
316321
}
317322

318323
/**
@@ -585,6 +590,11 @@ function loadLicenseCache() {
585590
* @returns {string} SHA-256 machine fingerprint (64 hex chars)
586591
*/
587592
function generateMachineId() {
593+
const machineIdModule = loadProModule('machine-id');
594+
if (machineIdModule && typeof machineIdModule.generateMachineId === 'function') {
595+
return machineIdModule.generateMachineId();
596+
}
597+
588598
const licenseCryptoModule = loadProModule('license-crypto');
589599
if (licenseCryptoModule && typeof licenseCryptoModule.generateMachineId === 'function') {
590600
return licenseCryptoModule.generateMachineId();
@@ -617,6 +627,39 @@ function generateMachineId() {
617627
return crypto.createHash('sha256').update(components.join('|')).digest('hex');
618628
}
619629

630+
/**
631+
* @returns {'persisted'|'native'|'legacy'|null}
632+
*/
633+
function getMachineIdSource() {
634+
const machineIdModule = loadProModule('machine-id');
635+
if (machineIdModule && typeof machineIdModule.getMachineIdSource === 'function') {
636+
return machineIdModule.getMachineIdSource();
637+
}
638+
639+
const licenseCryptoModule = loadProModule('license-crypto');
640+
if (licenseCryptoModule && typeof licenseCryptoModule.getMachineIdSource === 'function') {
641+
return licenseCryptoModule.getMachineIdSource();
642+
}
643+
644+
return null;
645+
}
646+
647+
/**
648+
* @param {Record<string, unknown>} payload
649+
* @returns {Record<string, unknown>}
650+
*/
651+
function withMachineIdSource(payload) {
652+
const machineIdSource = getMachineIdSource();
653+
if (!machineIdSource) {
654+
return payload;
655+
}
656+
657+
return {
658+
...payload,
659+
machineIdSource,
660+
};
661+
}
662+
620663
/**
621664
* Get a license API client instance.
622665
*

pro

Submodule pro updated from a9213d3 to 191de3d

tests/license/license-crypto.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
const crypto = require('crypto');
1111
const {
1212
generateMachineId,
13+
getMachineIdSource,
1314
deriveCacheKey,
1415
encrypt,
1516
decrypt,
@@ -18,10 +19,25 @@ const {
1819
verifyHMAC,
1920
maskKey,
2021
validateKeyFormat,
22+
_resetMachineIdCacheForTests,
2123
_CONFIG,
2224
} = require('../../pro/license/license-crypto');
25+
const {
26+
writePersistedMachineId,
27+
_resetMachineIdStoreForTests,
28+
} = require('../../pro/license/machine-id-store');
2329

2430
describe('license-crypto', () => {
31+
beforeEach(() => {
32+
_resetMachineIdCacheForTests();
33+
_resetMachineIdStoreForTests();
34+
});
35+
36+
afterEach(() => {
37+
_resetMachineIdCacheForTests();
38+
_resetMachineIdStoreForTests();
39+
});
40+
2541
describe('generateMachineId', () => {
2642
it('should generate a deterministic machine ID', () => {
2743
const id1 = generateMachineId();
@@ -42,6 +58,14 @@ describe('license-crypto', () => {
4258
const id = generateMachineId();
4359
expect(id).not.toBe('');
4460
});
61+
62+
it('should report persisted source when machine.id exists', () => {
63+
const persistedId = 'b'.repeat(64);
64+
writePersistedMachineId(persistedId);
65+
66+
expect(generateMachineId()).toBe(persistedId);
67+
expect(getMachineIdSource()).toBe('persisted');
68+
});
4569
});
4670

4771
describe('generateSalt', () => {

0 commit comments

Comments
 (0)