aiox-core 5.0.0 → 5.0.2
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/.aiox-core/data/entity-registry.yaml +5297 -1814
- package/.aiox-core/data/registry-update-log.jsonl +2 -0
- package/.aiox-core/development/templates/service-template/README.md.hbs +158 -158
- package/.aiox-core/development/templates/service-template/__tests__/index.test.ts.hbs +237 -237
- package/.aiox-core/development/templates/service-template/client.ts.hbs +403 -403
- package/.aiox-core/development/templates/service-template/errors.ts.hbs +182 -182
- package/.aiox-core/development/templates/service-template/index.ts.hbs +120 -120
- package/.aiox-core/development/templates/service-template/package.json.hbs +87 -87
- package/.aiox-core/development/templates/service-template/types.ts.hbs +145 -145
- package/.aiox-core/development/templates/squad-template/LICENSE +21 -21
- package/.aiox-core/infrastructure/scripts/tool-resolver.js +4 -4
- package/.aiox-core/infrastructure/templates/aiox-sync.yaml.template +182 -182
- package/.aiox-core/infrastructure/templates/coderabbit.yaml.template +279 -279
- package/.aiox-core/infrastructure/templates/github-workflows/ci.yml.template +169 -169
- package/.aiox-core/infrastructure/templates/github-workflows/pr-automation.yml.template +330 -330
- package/.aiox-core/infrastructure/templates/github-workflows/release.yml.template +196 -196
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-aiox-base.tmpl +63 -63
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl +18 -18
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-node.tmpl +85 -85
- package/.aiox-core/infrastructure/templates/gitignore/gitignore-python.tmpl +145 -145
- package/.aiox-core/install-manifest.yaml +58 -58
- package/.aiox-core/local-config.yaml.template +71 -71
- package/.aiox-core/monitor/hooks/lib/__init__.py +1 -1
- package/.aiox-core/monitor/hooks/lib/enrich.py +58 -58
- package/.aiox-core/monitor/hooks/lib/send_event.py +47 -47
- package/.aiox-core/monitor/hooks/notification.py +29 -29
- package/.aiox-core/monitor/hooks/post_tool_use.py +45 -45
- package/.aiox-core/monitor/hooks/pre_compact.py +29 -29
- package/.aiox-core/monitor/hooks/pre_tool_use.py +40 -40
- package/.aiox-core/monitor/hooks/stop.py +29 -29
- package/.aiox-core/monitor/hooks/subagent_stop.py +29 -29
- package/.aiox-core/monitor/hooks/user_prompt_submit.py +38 -38
- package/.aiox-core/product/templates/adr.hbs +125 -125
- package/.aiox-core/product/templates/dbdr.hbs +241 -241
- package/.aiox-core/product/templates/engine/elicitation.js +2 -3
- package/.aiox-core/product/templates/epic.hbs +212 -212
- package/.aiox-core/product/templates/pmdr.hbs +186 -186
- package/.aiox-core/product/templates/prd-v2.0.hbs +216 -216
- package/.aiox-core/product/templates/prd.hbs +201 -201
- package/.aiox-core/product/templates/story.hbs +263 -263
- package/.aiox-core/product/templates/task.hbs +170 -170
- package/.aiox-core/product/templates/tmpl-comment-on-examples.sql +158 -158
- package/.aiox-core/product/templates/tmpl-migration-script.sql +91 -91
- package/.aiox-core/product/templates/tmpl-rls-granular-policies.sql +104 -104
- package/.aiox-core/product/templates/tmpl-rls-kiss-policy.sql +10 -10
- package/.aiox-core/product/templates/tmpl-rls-roles.sql +135 -135
- package/.aiox-core/product/templates/tmpl-rls-simple.sql +77 -77
- package/.aiox-core/product/templates/tmpl-rls-tenant.sql +152 -152
- package/.aiox-core/product/templates/tmpl-rollback-script.sql +77 -77
- package/.aiox-core/product/templates/tmpl-seed-data.sql +140 -140
- package/.aiox-core/product/templates/tmpl-smoke-test.sql +16 -16
- package/.aiox-core/product/templates/tmpl-staging-copy-merge.sql +139 -139
- package/.aiox-core/product/templates/tmpl-stored-proc.sql +140 -140
- package/.aiox-core/product/templates/tmpl-trigger.sql +152 -152
- package/.aiox-core/product/templates/tmpl-view-materialized.sql +133 -133
- package/.aiox-core/product/templates/tmpl-view.sql +177 -177
- package/.aiox-core/scripts/pm.sh +0 -0
- package/.claude/hooks/code-intel-pretool.cjs +107 -0
- package/.claude/hooks/enforce-architecture-first.py +196 -196
- package/.claude/hooks/mind-clone-governance.py +192 -192
- package/.claude/hooks/read-protection.py +151 -151
- package/.claude/hooks/slug-validation.py +176 -176
- package/.claude/hooks/sql-governance.py +182 -182
- package/.claude/hooks/write-path-validation.py +194 -194
- package/LICENSE +33 -33
- package/bin/aiox-graph.js +0 -0
- package/bin/aiox-minimal.js +0 -0
- package/bin/aiox.js +0 -0
- package/docs/guides/aios-workflows/README.md +247 -0
- package/docs/guides/aios-workflows/bob-orchestrator-workflow.md +1536 -0
- package/package.json +1 -1
- package/packages/aiox-install/bin/aiox-install.js +0 -0
- package/packages/aiox-install/bin/edmcp.js +0 -0
- package/packages/aiox-pro-cli/bin/aiox-pro.js +0 -0
- package/packages/installer/src/wizard/pro-setup.js +210 -123
- package/pro/README.md +66 -0
- package/pro/license/degradation.js +220 -0
- package/pro/license/errors.js +450 -0
- package/pro/license/feature-gate.js +354 -0
- package/pro/license/index.js +181 -0
- package/pro/license/license-api.js +679 -0
- package/pro/license/license-cache.js +523 -0
- package/pro/license/license-crypto.js +303 -0
- package/scripts/check-markdown-links.py +352 -352
- package/scripts/dashboard-parallel-dev.sh +0 -0
- package/scripts/dashboard-parallel-phase3.sh +0 -0
- package/scripts/dashboard-parallel-phase4.sh +0 -0
- package/scripts/glue/README.md +355 -0
- package/scripts/glue/compose-agent-prompt.cjs +362 -0
- package/scripts/install-monitor-hooks.sh +0 -0
- package/.aiox-core/lib/build.json +0 -1
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License API Client
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for communicating with the license validation server.
|
|
5
|
+
* Supports activation, validation, deactivation, and offline sync.
|
|
6
|
+
*
|
|
7
|
+
* @module pro/license/license-api
|
|
8
|
+
* @see ADR-PRO-003 - Feature Gating & Licensing
|
|
9
|
+
* @see Story PRO-6 - License Key & Feature Gating System
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const https = require('https');
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const { URL } = require('url');
|
|
17
|
+
const { LicenseActivationError, AuthError, BuyerValidationError } = require('./errors');
|
|
18
|
+
const { hasPendingDeactivation, clearPendingDeactivation } = require('./license-cache');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default configuration.
|
|
22
|
+
*/
|
|
23
|
+
const CONFIG = {
|
|
24
|
+
BASE_URL: process.env.AIOX_LICENSE_API_URL || 'https://aios-license-server.vercel.app',
|
|
25
|
+
TIMEOUT_MS: 10000,
|
|
26
|
+
MAX_RETRIES: 3,
|
|
27
|
+
RETRY_DELAY_MS: 1000,
|
|
28
|
+
USER_AGENT: 'AIOX-Pro-License-Client/1.0',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* LicenseApiClient - HTTP client for license operations.
|
|
33
|
+
*/
|
|
34
|
+
class LicenseApiClient {
|
|
35
|
+
/**
|
|
36
|
+
* Create a LicenseApiClient.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} [options] - Client options
|
|
39
|
+
* @param {string} [options.baseUrl] - Base URL for API (default: api.synkra.ai)
|
|
40
|
+
* @param {number} [options.timeoutMs] - Request timeout in ms (default: 10000)
|
|
41
|
+
*/
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.baseUrl = options.baseUrl || CONFIG.BASE_URL;
|
|
44
|
+
this.timeoutMs = options.timeoutMs || CONFIG.TIMEOUT_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Make an HTTP request with timeout and abort support.
|
|
49
|
+
*
|
|
50
|
+
* @private
|
|
51
|
+
* @param {string} method - HTTP method
|
|
52
|
+
* @param {string} path - API path
|
|
53
|
+
* @param {object} body - Request body
|
|
54
|
+
* @returns {Promise<object>} Response data
|
|
55
|
+
* @throws {LicenseActivationError} On network or server errors
|
|
56
|
+
*/
|
|
57
|
+
async _request(method, path, body) {
|
|
58
|
+
const url = new URL(path, this.baseUrl);
|
|
59
|
+
const isHttps = url.protocol === 'https:';
|
|
60
|
+
const client = isHttps ? https : http;
|
|
61
|
+
|
|
62
|
+
const requestData = JSON.stringify(body);
|
|
63
|
+
|
|
64
|
+
const options = {
|
|
65
|
+
method,
|
|
66
|
+
hostname: url.hostname,
|
|
67
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
68
|
+
path: url.pathname + url.search,
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Content-Length': Buffer.byteLength(requestData),
|
|
72
|
+
'User-Agent': CONFIG.USER_AGENT,
|
|
73
|
+
Accept: 'application/json',
|
|
74
|
+
},
|
|
75
|
+
timeout: this.timeoutMs,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const req = client.request(options, (res) => {
|
|
80
|
+
let data = '';
|
|
81
|
+
|
|
82
|
+
res.on('data', (chunk) => {
|
|
83
|
+
data += chunk;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
res.on('end', () => {
|
|
87
|
+
try {
|
|
88
|
+
const response = data ? JSON.parse(data) : {};
|
|
89
|
+
this._handleResponse(res.statusCode, response, resolve, reject);
|
|
90
|
+
} catch {
|
|
91
|
+
reject(
|
|
92
|
+
new LicenseActivationError(
|
|
93
|
+
'Invalid response from license server',
|
|
94
|
+
'INVALID_RESPONSE',
|
|
95
|
+
{ rawData: data.substring(0, 200) },
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Handle request timeout
|
|
103
|
+
req.on('timeout', () => {
|
|
104
|
+
req.destroy();
|
|
105
|
+
reject(LicenseActivationError.networkError(new Error('Request timeout')));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Handle request error
|
|
109
|
+
req.on('error', (error) => {
|
|
110
|
+
reject(LicenseActivationError.networkError(error));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Send request body
|
|
114
|
+
req.write(requestData);
|
|
115
|
+
req.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle HTTP response based on status code.
|
|
121
|
+
*
|
|
122
|
+
* @private
|
|
123
|
+
* @param {number} statusCode - HTTP status code
|
|
124
|
+
* @param {object} response - Parsed response body
|
|
125
|
+
* @param {Function} resolve - Promise resolve
|
|
126
|
+
* @param {Function} reject - Promise reject
|
|
127
|
+
*/
|
|
128
|
+
_handleResponse(statusCode, response, resolve, reject) {
|
|
129
|
+
// Success
|
|
130
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
131
|
+
resolve(response);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Normalize server error envelope: { error: { code, message, details } } → flat
|
|
136
|
+
const err = response.error || response;
|
|
137
|
+
const code = err.code;
|
|
138
|
+
const message = err.message;
|
|
139
|
+
const details = err.details;
|
|
140
|
+
|
|
141
|
+
// Client errors
|
|
142
|
+
switch (statusCode) {
|
|
143
|
+
case 400:
|
|
144
|
+
reject(
|
|
145
|
+
new LicenseActivationError(
|
|
146
|
+
message || 'Invalid request',
|
|
147
|
+
code || 'BAD_REQUEST',
|
|
148
|
+
details,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 401:
|
|
154
|
+
// Preserve server error code (e.g., INVALID_CREDENTIALS, EMAIL_NOT_VERIFIED)
|
|
155
|
+
reject(
|
|
156
|
+
new LicenseActivationError(
|
|
157
|
+
message || 'Unauthorized',
|
|
158
|
+
code || 'INVALID_KEY',
|
|
159
|
+
details,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 409:
|
|
165
|
+
reject(
|
|
166
|
+
new LicenseActivationError(
|
|
167
|
+
message || 'Conflict',
|
|
168
|
+
code || 'CONFLICT',
|
|
169
|
+
details,
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 403:
|
|
175
|
+
if (code === 'EXPIRED_KEY') {
|
|
176
|
+
reject(LicenseActivationError.expiredKey());
|
|
177
|
+
} else if (code === 'SEAT_LIMIT_EXCEEDED') {
|
|
178
|
+
reject(
|
|
179
|
+
LicenseActivationError.seatLimitExceeded(
|
|
180
|
+
details?.used || 0,
|
|
181
|
+
details?.max || 0,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
} else {
|
|
185
|
+
reject(
|
|
186
|
+
new LicenseActivationError(
|
|
187
|
+
message || 'Access forbidden',
|
|
188
|
+
code || 'FORBIDDEN',
|
|
189
|
+
details,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case 429:
|
|
196
|
+
reject(LicenseActivationError.rateLimited(err.retryAfter || response.retryAfter));
|
|
197
|
+
break;
|
|
198
|
+
|
|
199
|
+
case 500:
|
|
200
|
+
case 502:
|
|
201
|
+
case 503:
|
|
202
|
+
case 504:
|
|
203
|
+
// Preserve server error code if provided (e.g., BUYER_SERVICE_UNAVAILABLE)
|
|
204
|
+
if (code) {
|
|
205
|
+
reject(
|
|
206
|
+
new LicenseActivationError(
|
|
207
|
+
message || 'Server error',
|
|
208
|
+
code,
|
|
209
|
+
details,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
reject(LicenseActivationError.serverError());
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
default:
|
|
218
|
+
// Preserve server error code/message when available
|
|
219
|
+
reject(
|
|
220
|
+
new LicenseActivationError(
|
|
221
|
+
message || `Unexpected response: ${statusCode}`,
|
|
222
|
+
code || 'UNEXPECTED_STATUS',
|
|
223
|
+
details || { statusCode, response },
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Activate a license key.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} key - License key (PRO-XXXX-XXXX-XXXX-XXXX)
|
|
233
|
+
* @param {string} machineId - Machine fingerprint
|
|
234
|
+
* @param {string} aiosCoreVersion - AIOX Core version
|
|
235
|
+
* @returns {Promise<object>} Activation result with features, seats, cache info
|
|
236
|
+
* @throws {LicenseActivationError} On activation failure
|
|
237
|
+
*/
|
|
238
|
+
async activate(key, machineId, aiosCoreVersion) {
|
|
239
|
+
// First, sync any pending deactivations
|
|
240
|
+
await this.syncPendingDeactivation(machineId);
|
|
241
|
+
|
|
242
|
+
const response = await this._request('POST', '/v1/license/activate', {
|
|
243
|
+
key,
|
|
244
|
+
machineId,
|
|
245
|
+
aiosCoreVersion,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Validate response structure
|
|
249
|
+
if (!response.features || !Array.isArray(response.features)) {
|
|
250
|
+
throw new LicenseActivationError(
|
|
251
|
+
'Invalid activation response: missing features',
|
|
252
|
+
'INVALID_RESPONSE',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
key: response.key || key,
|
|
258
|
+
features: response.features,
|
|
259
|
+
seats: response.seats || { used: 1, max: 1 },
|
|
260
|
+
expiresAt: response.expiresAt,
|
|
261
|
+
cacheValidDays: response.cacheValidDays || 30,
|
|
262
|
+
gracePeriodDays: response.gracePeriodDays || 7,
|
|
263
|
+
activatedAt: new Date().toISOString(),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Validate an existing license.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} key - License key
|
|
271
|
+
* @param {string} machineId - Machine fingerprint
|
|
272
|
+
* @returns {Promise<object>} Validation result
|
|
273
|
+
* @throws {LicenseActivationError} On validation failure
|
|
274
|
+
*/
|
|
275
|
+
async validate(key, machineId) {
|
|
276
|
+
// First, sync any pending deactivations
|
|
277
|
+
await this.syncPendingDeactivation(machineId);
|
|
278
|
+
|
|
279
|
+
const response = await this._request('POST', '/v1/license/validate', {
|
|
280
|
+
key,
|
|
281
|
+
machineId,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
valid: response.valid !== false,
|
|
286
|
+
features: response.features || [],
|
|
287
|
+
seats: response.seats || { used: 1, max: 1 },
|
|
288
|
+
expiresAt: response.expiresAt,
|
|
289
|
+
cacheValidDays: response.cacheValidDays || 30,
|
|
290
|
+
gracePeriodDays: response.gracePeriodDays || 7,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Deactivate a license from this machine.
|
|
296
|
+
*
|
|
297
|
+
* @param {string} key - License key
|
|
298
|
+
* @param {string} machineId - Machine fingerprint
|
|
299
|
+
* @returns {Promise<object>} Deactivation result
|
|
300
|
+
* @throws {LicenseActivationError} On deactivation failure
|
|
301
|
+
*/
|
|
302
|
+
async deactivate(key, machineId) {
|
|
303
|
+
const response = await this._request('POST', '/v1/license/deactivate', {
|
|
304
|
+
key,
|
|
305
|
+
machineId,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
success: response.success !== false,
|
|
310
|
+
seatFreed: response.seatFreed !== false,
|
|
311
|
+
message: response.message || 'License deactivated successfully',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Sync any pending offline deactivations with the server.
|
|
317
|
+
*
|
|
318
|
+
* This is called automatically before activate/validate operations.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} machineId - Current machine ID for verification
|
|
321
|
+
* @param {string} [baseDir] - Base directory for cache (optional)
|
|
322
|
+
* @returns {Promise<boolean>} true if sync was performed
|
|
323
|
+
*/
|
|
324
|
+
async syncPendingDeactivation(machineId, baseDir) {
|
|
325
|
+
try {
|
|
326
|
+
const pendingResult = hasPendingDeactivation(baseDir);
|
|
327
|
+
|
|
328
|
+
if (!pendingResult.pending) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const pending = pendingResult.data;
|
|
333
|
+
|
|
334
|
+
if (!pending || !pending.licenseKey) {
|
|
335
|
+
// Invalid pending data, clear it
|
|
336
|
+
clearPendingDeactivation(baseDir);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Attempt to deactivate on server
|
|
341
|
+
try {
|
|
342
|
+
await this._request('POST', '/v1/license/deactivate', {
|
|
343
|
+
key: pending.licenseKey,
|
|
344
|
+
machineId: pending.machineId || machineId,
|
|
345
|
+
offlineDeactivation: true,
|
|
346
|
+
offlineTimestamp: pending.deactivatedAt,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Success - clear the pending flag
|
|
350
|
+
clearPendingDeactivation(baseDir);
|
|
351
|
+
return true;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
// If the key is already deactivated or invalid, clear pending
|
|
354
|
+
if (error.code === 'INVALID_KEY' || error.code === 'NOT_ACTIVATED') {
|
|
355
|
+
clearPendingDeactivation(baseDir);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// For other errors (network, server), keep pending for next sync
|
|
360
|
+
// Don't throw - allow the main operation to continue
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
// If we can't read pending state, continue anyway
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ────────────────────────────────────────────────────────────────
|
|
370
|
+
// Auth methods (Story PRO-11 - Email Authentication)
|
|
371
|
+
// ────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Pre-flight check: verify buyer status and account existence for an email.
|
|
375
|
+
*
|
|
376
|
+
* @param {string} email - User email
|
|
377
|
+
* @returns {Promise<object>} Result with { isBuyer, hasAccount, email }
|
|
378
|
+
* @throws {AuthError} On check failure
|
|
379
|
+
*/
|
|
380
|
+
async checkEmail(email) {
|
|
381
|
+
try {
|
|
382
|
+
const response = await this._request('POST', '/api/v1/auth/check-email', {
|
|
383
|
+
email,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
isBuyer: response.isBuyer === true,
|
|
388
|
+
hasAccount: response.hasAccount === true,
|
|
389
|
+
email: response.email || email,
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
if (error.code === 'RATE_LIMITED') {
|
|
393
|
+
throw AuthError.rateLimited(error.details?.retryAfter);
|
|
394
|
+
}
|
|
395
|
+
throw new AuthError(
|
|
396
|
+
error.message || 'Failed to check email',
|
|
397
|
+
error.code || 'CHECK_EMAIL_FAILED',
|
|
398
|
+
error.details,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Register a new user with email and password.
|
|
405
|
+
*
|
|
406
|
+
* @param {string} email - User email
|
|
407
|
+
* @param {string} password - User password (min 8 characters)
|
|
408
|
+
* @returns {Promise<object>} Signup result with { userId, message }
|
|
409
|
+
* @throws {AuthError} On signup failure
|
|
410
|
+
*/
|
|
411
|
+
async signup(email, password) {
|
|
412
|
+
try {
|
|
413
|
+
const response = await this._request('POST', '/api/v1/auth/signup', {
|
|
414
|
+
email,
|
|
415
|
+
password,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
userId: response.userId,
|
|
420
|
+
message: response.message || 'Verification email sent. Please check your inbox.',
|
|
421
|
+
};
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (
|
|
424
|
+
error.code === 'EMAIL_ALREADY_REGISTERED' ||
|
|
425
|
+
(error.code === 'BAD_REQUEST' && error.message.includes('already'))
|
|
426
|
+
) {
|
|
427
|
+
throw AuthError.emailAlreadyRegistered();
|
|
428
|
+
}
|
|
429
|
+
if (error.code === 'RATE_LIMITED') {
|
|
430
|
+
throw AuthError.rateLimited(error.details?.retryAfter);
|
|
431
|
+
}
|
|
432
|
+
throw new AuthError(
|
|
433
|
+
error.message || 'Signup failed',
|
|
434
|
+
error.code || 'SIGNUP_FAILED',
|
|
435
|
+
error.details,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Login with email and password.
|
|
442
|
+
*
|
|
443
|
+
* @param {string} email - User email
|
|
444
|
+
* @param {string} password - User password
|
|
445
|
+
* @returns {Promise<object>} Login result with { sessionToken, userId, emailVerified }
|
|
446
|
+
* @throws {AuthError} On login failure
|
|
447
|
+
*/
|
|
448
|
+
async login(email, password) {
|
|
449
|
+
try {
|
|
450
|
+
const response = await this._request('POST', '/api/v1/auth/login', {
|
|
451
|
+
email,
|
|
452
|
+
password,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
sessionToken: response.accessToken,
|
|
457
|
+
userId: response.userId,
|
|
458
|
+
emailVerified: response.emailVerified !== false,
|
|
459
|
+
};
|
|
460
|
+
} catch (error) {
|
|
461
|
+
if (error.code === 'EMAIL_NOT_VERIFIED') {
|
|
462
|
+
throw AuthError.emailNotVerified();
|
|
463
|
+
}
|
|
464
|
+
if (
|
|
465
|
+
error.code === 'INVALID_KEY' ||
|
|
466
|
+
error.code === 'INVALID_CREDENTIALS' ||
|
|
467
|
+
error.code === 'BAD_REQUEST'
|
|
468
|
+
) {
|
|
469
|
+
throw AuthError.invalidCredentials();
|
|
470
|
+
}
|
|
471
|
+
if (error.code === 'RATE_LIMITED') {
|
|
472
|
+
throw AuthError.rateLimited(error.details?.retryAfter);
|
|
473
|
+
}
|
|
474
|
+
throw new AuthError(
|
|
475
|
+
error.message || 'Login failed',
|
|
476
|
+
error.code || 'LOGIN_FAILED',
|
|
477
|
+
error.details,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Check if user's email has been verified.
|
|
484
|
+
*
|
|
485
|
+
* @param {string} sessionToken - Session token from login/signup
|
|
486
|
+
* @returns {Promise<object>} Status with { verified, email }
|
|
487
|
+
* @throws {AuthError} On verification check failure
|
|
488
|
+
*/
|
|
489
|
+
async checkEmailVerified(sessionToken) {
|
|
490
|
+
try {
|
|
491
|
+
const response = await this._request('POST', '/api/v1/auth/verify-status', {
|
|
492
|
+
accessToken: sessionToken,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
verified: response.emailVerified === true,
|
|
497
|
+
email: response.email,
|
|
498
|
+
};
|
|
499
|
+
} catch (error) {
|
|
500
|
+
throw new AuthError(
|
|
501
|
+
error.message || 'Failed to check email verification status',
|
|
502
|
+
error.code || 'VERIFY_CHECK_FAILED',
|
|
503
|
+
error.details,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Activate Pro via authenticated session.
|
|
510
|
+
*
|
|
511
|
+
* Server-side flow: verify session → check email verified → validate buyer → generate/recover key → activate.
|
|
512
|
+
*
|
|
513
|
+
* @param {string} sessionToken - Session token from login
|
|
514
|
+
* @param {string} machineId - Machine fingerprint
|
|
515
|
+
* @param {string} aiosCoreVersion - AIOX Core version
|
|
516
|
+
* @returns {Promise<object>} Activation result (same shape as activate())
|
|
517
|
+
* @throws {AuthError} On auth failure
|
|
518
|
+
* @throws {BuyerValidationError} If user is not a buyer
|
|
519
|
+
* @throws {LicenseActivationError} On activation failure
|
|
520
|
+
*/
|
|
521
|
+
async activateByAuth(sessionToken, machineId, aiosCoreVersion) {
|
|
522
|
+
try {
|
|
523
|
+
const response = await this._request('POST', '/api/v1/auth/activate-pro', {
|
|
524
|
+
accessToken: sessionToken,
|
|
525
|
+
machineId,
|
|
526
|
+
aiosCoreVersion,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (!response.features || !Array.isArray(response.features)) {
|
|
530
|
+
throw new LicenseActivationError(
|
|
531
|
+
'Invalid activation response: missing features',
|
|
532
|
+
'INVALID_RESPONSE',
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
key: response.licenseKey || response.key,
|
|
538
|
+
features: response.features,
|
|
539
|
+
seats: response.seats || { used: 1, max: 3 },
|
|
540
|
+
expiresAt: response.expiresAt,
|
|
541
|
+
cacheValidDays: response.cacheValidDays || 30,
|
|
542
|
+
gracePeriodDays: response.gracePeriodDays || 7,
|
|
543
|
+
activatedAt: new Date().toISOString(),
|
|
544
|
+
};
|
|
545
|
+
} catch (error) {
|
|
546
|
+
// Re-throw typed errors
|
|
547
|
+
if (error instanceof AuthError || error instanceof BuyerValidationError) {
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (error.code === 'EMAIL_NOT_VERIFIED') {
|
|
552
|
+
throw AuthError.emailNotVerified();
|
|
553
|
+
}
|
|
554
|
+
if (error.code === 'NOT_A_BUYER') {
|
|
555
|
+
throw BuyerValidationError.notABuyer();
|
|
556
|
+
}
|
|
557
|
+
if (error.code === 'BUYER_SERVICE_UNAVAILABLE') {
|
|
558
|
+
throw BuyerValidationError.serviceUnavailable();
|
|
559
|
+
}
|
|
560
|
+
if (error.code === 'SEAT_LIMIT_EXCEEDED') {
|
|
561
|
+
throw LicenseActivationError.seatLimitExceeded(
|
|
562
|
+
error.details?.used || 0,
|
|
563
|
+
error.details?.max || 0,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
throw new AuthError(
|
|
568
|
+
error.message || 'Pro activation failed',
|
|
569
|
+
error.code || 'ACTIVATE_PRO_FAILED',
|
|
570
|
+
error.details,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Request a password reset email via Supabase.
|
|
577
|
+
*
|
|
578
|
+
* @param {string} email - User email address
|
|
579
|
+
* @returns {Promise<object>} Result with { message }
|
|
580
|
+
* @throws {AuthError} On failure
|
|
581
|
+
*/
|
|
582
|
+
async requestPasswordReset(email) {
|
|
583
|
+
try {
|
|
584
|
+
const response = await this._request('POST', '/api/v1/auth/request-reset', {
|
|
585
|
+
email,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
message: response.message || 'If this email is associated with an account, you will receive reset instructions.',
|
|
590
|
+
};
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (error.code === 'RATE_LIMITED') {
|
|
593
|
+
throw AuthError.rateLimited(error.details?.retryAfter);
|
|
594
|
+
}
|
|
595
|
+
throw new AuthError(
|
|
596
|
+
error.message || 'Failed to request password reset',
|
|
597
|
+
error.code || 'REQUEST_RESET_FAILED',
|
|
598
|
+
error.details,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Resend email verification.
|
|
605
|
+
*
|
|
606
|
+
* @param {string} email - User email address
|
|
607
|
+
* @returns {Promise<object>} Result with { message }
|
|
608
|
+
* @throws {AuthError} On failure
|
|
609
|
+
*/
|
|
610
|
+
async resendVerification(email) {
|
|
611
|
+
try {
|
|
612
|
+
const response = await this._request('POST', '/api/v1/auth/resend-verification', {
|
|
613
|
+
email,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
message: response.message || 'Verification email resent.',
|
|
618
|
+
};
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (error.code === 'RATE_LIMITED') {
|
|
621
|
+
throw AuthError.rateLimited(error.details?.retryAfter);
|
|
622
|
+
}
|
|
623
|
+
throw new AuthError(
|
|
624
|
+
error.message || 'Failed to resend verification email',
|
|
625
|
+
error.code || 'RESEND_FAILED',
|
|
626
|
+
error.details,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Check if the API server is reachable.
|
|
633
|
+
*
|
|
634
|
+
* @returns {Promise<boolean>} true if server is reachable
|
|
635
|
+
*/
|
|
636
|
+
async isOnline() {
|
|
637
|
+
try {
|
|
638
|
+
const url = new URL('/health', this.baseUrl);
|
|
639
|
+
const isHttps = url.protocol === 'https:';
|
|
640
|
+
const client = isHttps ? https : http;
|
|
641
|
+
|
|
642
|
+
return new Promise((resolve) => {
|
|
643
|
+
const req = client.request(
|
|
644
|
+
{
|
|
645
|
+
method: 'HEAD',
|
|
646
|
+
hostname: url.hostname,
|
|
647
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
648
|
+
path: url.pathname,
|
|
649
|
+
timeout: 3000, // Quick check
|
|
650
|
+
},
|
|
651
|
+
(res) => {
|
|
652
|
+
resolve(res.statusCode < 500);
|
|
653
|
+
},
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
req.on('timeout', () => {
|
|
657
|
+
req.destroy();
|
|
658
|
+
resolve(false);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
req.on('error', () => {
|
|
662
|
+
resolve(false);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
req.end();
|
|
666
|
+
});
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Singleton instance
|
|
674
|
+
const licenseApi = new LicenseApiClient();
|
|
675
|
+
|
|
676
|
+
module.exports = {
|
|
677
|
+
LicenseApiClient,
|
|
678
|
+
licenseApi,
|
|
679
|
+
};
|