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,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License Cache Module
|
|
3
|
+
*
|
|
4
|
+
* Manages encrypted license cache file operations:
|
|
5
|
+
* - Write encrypted cache with HMAC integrity
|
|
6
|
+
* - Read and verify cache with tamper detection
|
|
7
|
+
* - Expiry and grace period calculations
|
|
8
|
+
* - Atomic file operations for data safety
|
|
9
|
+
* - Pending deactivation tracking for offline scenarios
|
|
10
|
+
*
|
|
11
|
+
* Cache file: .aiox/license.cache (encrypted, gitignored)
|
|
12
|
+
*
|
|
13
|
+
* @module pro/license/license-cache
|
|
14
|
+
* @see ADR-PRO-003 - Feature Gating & Licensing
|
|
15
|
+
* @see Story PRO-6 - License Key & Feature Gating System
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const {
|
|
23
|
+
generateMachineId,
|
|
24
|
+
generateSalt,
|
|
25
|
+
deriveCacheKey,
|
|
26
|
+
encrypt,
|
|
27
|
+
decrypt,
|
|
28
|
+
computeHMAC,
|
|
29
|
+
verifyHMAC,
|
|
30
|
+
} = require('./license-crypto');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Configuration constants for cache operations.
|
|
34
|
+
*/
|
|
35
|
+
const CONFIG = {
|
|
36
|
+
// Default expiry settings (per ADR-PRO-003)
|
|
37
|
+
DEFAULT_CACHE_VALID_DAYS: 30,
|
|
38
|
+
DEFAULT_GRACE_PERIOD_DAYS: 7,
|
|
39
|
+
|
|
40
|
+
// File paths
|
|
41
|
+
AIOX_DIR: '.aiox',
|
|
42
|
+
CACHE_FILENAME: 'license.cache',
|
|
43
|
+
PENDING_DEACTIVATION_FILENAME: 'pending-deactivation.json',
|
|
44
|
+
|
|
45
|
+
// Cache version for migration support
|
|
46
|
+
CACHE_VERSION: 1,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the directory path for AIOX cache files.
|
|
51
|
+
*
|
|
52
|
+
* Uses project root (cwd) by default, falls back to home directory.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
55
|
+
* @returns {string} Full path to .aiox directory
|
|
56
|
+
*/
|
|
57
|
+
function getAioxDir(baseDir = process.cwd()) {
|
|
58
|
+
return path.join(baseDir, CONFIG.AIOX_DIR);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the full path to the license cache file.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
65
|
+
* @returns {string} Full path to license.cache
|
|
66
|
+
*/
|
|
67
|
+
function getCachePath(baseDir) {
|
|
68
|
+
return path.join(getAioxDir(baseDir), CONFIG.CACHE_FILENAME);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the full path to the pending deactivation file.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
75
|
+
* @returns {string} Full path to pending-deactivation.json
|
|
76
|
+
*/
|
|
77
|
+
function getPendingDeactivationPath(baseDir) {
|
|
78
|
+
return path.join(getAioxDir(baseDir), CONFIG.PENDING_DEACTIVATION_FILENAME);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ensure the .aiox directory exists.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
85
|
+
*/
|
|
86
|
+
function ensureAioxDir(baseDir) {
|
|
87
|
+
const aioxDir = getAioxDir(baseDir);
|
|
88
|
+
if (!fs.existsSync(aioxDir)) {
|
|
89
|
+
fs.mkdirSync(aioxDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write license cache to disk with encryption and integrity protection.
|
|
95
|
+
*
|
|
96
|
+
* Uses atomic write (temp file → rename) for crash safety.
|
|
97
|
+
*
|
|
98
|
+
* Cache format on disk:
|
|
99
|
+
* {
|
|
100
|
+
* "encrypted": "<AES-256-GCM ciphertext>",
|
|
101
|
+
* "iv": "<initialization vector>",
|
|
102
|
+
* "tag": "<auth tag>",
|
|
103
|
+
* "hmac": "<HMAC-SHA256 of encrypted content>",
|
|
104
|
+
* "salt": "<PBKDF2 salt>",
|
|
105
|
+
* "version": 1
|
|
106
|
+
* }
|
|
107
|
+
*
|
|
108
|
+
* @param {object} data - License data to cache
|
|
109
|
+
* @param {string} data.key - License key (PRO-XXXX-XXXX-XXXX-XXXX)
|
|
110
|
+
* @param {string} data.activatedAt - ISO timestamp of activation
|
|
111
|
+
* @param {string} data.expiresAt - ISO timestamp of expiry
|
|
112
|
+
* @param {string[]} data.features - Array of enabled feature IDs
|
|
113
|
+
* @param {object} data.seats - Seat usage info { used, max }
|
|
114
|
+
* @param {number} [data.cacheValidDays=30] - Days until cache expires
|
|
115
|
+
* @param {number} [data.gracePeriodDays=7] - Grace period after expiry
|
|
116
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
117
|
+
* @returns {{ success: boolean, error?: string }} Write result
|
|
118
|
+
*/
|
|
119
|
+
function writeLicenseCache(data, baseDir) {
|
|
120
|
+
try {
|
|
121
|
+
ensureAioxDir(baseDir);
|
|
122
|
+
|
|
123
|
+
// Generate machine-specific encryption key
|
|
124
|
+
const machineId = generateMachineId();
|
|
125
|
+
const salt = generateSalt();
|
|
126
|
+
const key = deriveCacheKey(machineId, salt);
|
|
127
|
+
|
|
128
|
+
// Add metadata to cache data
|
|
129
|
+
const cacheData = {
|
|
130
|
+
...data,
|
|
131
|
+
machineId, // Store for verification (hashed, not sensitive)
|
|
132
|
+
cacheValidDays: data.cacheValidDays || CONFIG.DEFAULT_CACHE_VALID_DAYS,
|
|
133
|
+
gracePeriodDays: data.gracePeriodDays || CONFIG.DEFAULT_GRACE_PERIOD_DAYS,
|
|
134
|
+
version: CONFIG.CACHE_VERSION,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Encrypt the data
|
|
138
|
+
const encrypted = encrypt(cacheData, key);
|
|
139
|
+
|
|
140
|
+
// Compute HMAC over encrypted content for integrity
|
|
141
|
+
const hmacData = JSON.stringify({
|
|
142
|
+
ciphertext: encrypted.ciphertext,
|
|
143
|
+
iv: encrypted.iv,
|
|
144
|
+
tag: encrypted.tag,
|
|
145
|
+
});
|
|
146
|
+
const hmac = computeHMAC(hmacData, key);
|
|
147
|
+
|
|
148
|
+
// Build cache file structure
|
|
149
|
+
const cacheFile = {
|
|
150
|
+
encrypted: encrypted.ciphertext,
|
|
151
|
+
iv: encrypted.iv,
|
|
152
|
+
tag: encrypted.tag,
|
|
153
|
+
hmac,
|
|
154
|
+
salt: salt.toString('hex'),
|
|
155
|
+
version: CONFIG.CACHE_VERSION,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Atomic write: temp file → rename
|
|
159
|
+
const cachePath = getCachePath(baseDir);
|
|
160
|
+
const tempPath = `${cachePath}.tmp.${process.pid}`;
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(tempPath, JSON.stringify(cacheFile, null, 2), 'utf8');
|
|
163
|
+
fs.renameSync(tempPath, cachePath);
|
|
164
|
+
|
|
165
|
+
return { success: true };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return { success: false, error: error.message };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Read and verify license cache from disk.
|
|
173
|
+
*
|
|
174
|
+
* Performs:
|
|
175
|
+
* 1. File existence check
|
|
176
|
+
* 2. JSON parsing
|
|
177
|
+
* 3. HMAC integrity verification
|
|
178
|
+
* 4. AES-256-GCM decryption with auth tag verification
|
|
179
|
+
* 5. Machine ID verification (cache non-portable)
|
|
180
|
+
*
|
|
181
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
182
|
+
* @returns {object|null} Decrypted cache data or null if invalid/missing
|
|
183
|
+
*/
|
|
184
|
+
function readLicenseCache(baseDir) {
|
|
185
|
+
try {
|
|
186
|
+
const cachePath = getCachePath(baseDir);
|
|
187
|
+
|
|
188
|
+
// Check file exists
|
|
189
|
+
if (!fs.existsSync(cachePath)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Read and parse
|
|
194
|
+
const fileContent = fs.readFileSync(cachePath, 'utf8');
|
|
195
|
+
const cacheFile = JSON.parse(fileContent);
|
|
196
|
+
|
|
197
|
+
// Validate structure
|
|
198
|
+
if (!cacheFile.encrypted || !cacheFile.iv || !cacheFile.tag || !cacheFile.hmac || !cacheFile.salt) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Derive key from current machine
|
|
203
|
+
const machineId = generateMachineId();
|
|
204
|
+
const salt = Buffer.from(cacheFile.salt, 'hex');
|
|
205
|
+
const key = deriveCacheKey(machineId, salt);
|
|
206
|
+
|
|
207
|
+
// Verify HMAC integrity
|
|
208
|
+
const hmacData = JSON.stringify({
|
|
209
|
+
ciphertext: cacheFile.encrypted,
|
|
210
|
+
iv: cacheFile.iv,
|
|
211
|
+
tag: cacheFile.tag,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!verifyHMAC(hmacData, key, cacheFile.hmac)) {
|
|
215
|
+
// HMAC mismatch: cache is tampered or from different machine
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Decrypt
|
|
220
|
+
const encryptedData = {
|
|
221
|
+
ciphertext: cacheFile.encrypted,
|
|
222
|
+
iv: cacheFile.iv,
|
|
223
|
+
tag: cacheFile.tag,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const decrypted = decrypt(encryptedData, key);
|
|
227
|
+
|
|
228
|
+
// Verify machine ID matches (defense in depth)
|
|
229
|
+
if (decrypted.machineId !== machineId) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return decrypted;
|
|
234
|
+
} catch {
|
|
235
|
+
// Any error (parse, decrypt, etc.) means cache is invalid
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Delete the license cache file.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
244
|
+
* @returns {{ success: boolean, error?: string }} Delete result
|
|
245
|
+
*/
|
|
246
|
+
function deleteLicenseCache(baseDir) {
|
|
247
|
+
try {
|
|
248
|
+
const cachePath = getCachePath(baseDir);
|
|
249
|
+
|
|
250
|
+
if (fs.existsSync(cachePath)) {
|
|
251
|
+
fs.unlinkSync(cachePath);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { success: true };
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return { success: false, error: error.message };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if the license cache is expired.
|
|
262
|
+
*
|
|
263
|
+
* Expired means: current date > activatedAt + cacheValidDays
|
|
264
|
+
*
|
|
265
|
+
* @param {object} cache - Decrypted cache data
|
|
266
|
+
* @returns {boolean} true if cache is expired
|
|
267
|
+
*/
|
|
268
|
+
function isExpired(cache) {
|
|
269
|
+
if (!cache || !cache.activatedAt) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const activatedAt = new Date(cache.activatedAt);
|
|
274
|
+
const cacheValidDays = cache.cacheValidDays || CONFIG.DEFAULT_CACHE_VALID_DAYS;
|
|
275
|
+
|
|
276
|
+
const expiryDate = new Date(activatedAt);
|
|
277
|
+
expiryDate.setDate(expiryDate.getDate() + cacheValidDays);
|
|
278
|
+
|
|
279
|
+
return new Date() > expiryDate;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if the license is in grace period.
|
|
284
|
+
*
|
|
285
|
+
* Grace period: cache is expired but within gracePeriodDays after expiry.
|
|
286
|
+
*
|
|
287
|
+
* @param {object} cache - Decrypted cache data
|
|
288
|
+
* @returns {boolean} true if in grace period
|
|
289
|
+
*/
|
|
290
|
+
function isInGracePeriod(cache) {
|
|
291
|
+
if (!cache || !cache.activatedAt) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Must be expired first
|
|
296
|
+
if (!isExpired(cache)) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const activatedAt = new Date(cache.activatedAt);
|
|
301
|
+
const cacheValidDays = cache.cacheValidDays || CONFIG.DEFAULT_CACHE_VALID_DAYS;
|
|
302
|
+
const gracePeriodDays = cache.gracePeriodDays || CONFIG.DEFAULT_GRACE_PERIOD_DAYS;
|
|
303
|
+
|
|
304
|
+
// Calculate grace period end
|
|
305
|
+
const expiryDate = new Date(activatedAt);
|
|
306
|
+
expiryDate.setDate(expiryDate.getDate() + cacheValidDays);
|
|
307
|
+
|
|
308
|
+
const graceEndDate = new Date(expiryDate);
|
|
309
|
+
graceEndDate.setDate(graceEndDate.getDate() + gracePeriodDays);
|
|
310
|
+
|
|
311
|
+
return new Date() <= graceEndDate;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get the number of days remaining until cache expires.
|
|
316
|
+
*
|
|
317
|
+
* @param {object} cache - Decrypted cache data
|
|
318
|
+
* @returns {number} Days remaining (negative if expired)
|
|
319
|
+
*/
|
|
320
|
+
function getDaysRemaining(cache) {
|
|
321
|
+
if (!cache || !cache.activatedAt) {
|
|
322
|
+
return -1;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const activatedAt = new Date(cache.activatedAt);
|
|
326
|
+
const cacheValidDays = cache.cacheValidDays || CONFIG.DEFAULT_CACHE_VALID_DAYS;
|
|
327
|
+
|
|
328
|
+
const expiryDate = new Date(activatedAt);
|
|
329
|
+
expiryDate.setDate(expiryDate.getDate() + cacheValidDays);
|
|
330
|
+
|
|
331
|
+
const now = new Date();
|
|
332
|
+
const diffMs = expiryDate - now;
|
|
333
|
+
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
|
334
|
+
|
|
335
|
+
return diffDays;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the expiry date of the cache.
|
|
340
|
+
*
|
|
341
|
+
* @param {object} cache - Decrypted cache data
|
|
342
|
+
* @returns {Date|null} Expiry date or null
|
|
343
|
+
*/
|
|
344
|
+
function getExpiryDate(cache) {
|
|
345
|
+
if (!cache || !cache.activatedAt) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const activatedAt = new Date(cache.activatedAt);
|
|
350
|
+
const cacheValidDays = cache.cacheValidDays || CONFIG.DEFAULT_CACHE_VALID_DAYS;
|
|
351
|
+
|
|
352
|
+
const expiryDate = new Date(activatedAt);
|
|
353
|
+
expiryDate.setDate(expiryDate.getDate() + cacheValidDays);
|
|
354
|
+
|
|
355
|
+
return expiryDate;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the license state based on cache.
|
|
360
|
+
*
|
|
361
|
+
* @param {object|null} cache - Decrypted cache data
|
|
362
|
+
* @returns {'Active'|'Expired'|'Grace'|'Not Activated'} License state
|
|
363
|
+
*/
|
|
364
|
+
function getLicenseState(cache) {
|
|
365
|
+
if (!cache) {
|
|
366
|
+
return 'Not Activated';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!isExpired(cache)) {
|
|
370
|
+
return 'Active';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isInGracePeriod(cache)) {
|
|
374
|
+
return 'Grace';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return 'Expired';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Store a pending deactivation flag.
|
|
382
|
+
*
|
|
383
|
+
* Used when user deactivates offline - the deactivation will
|
|
384
|
+
* be synced to the server on next online connection.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} licenseKey - The license key being deactivated
|
|
387
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
388
|
+
* @returns {{ success: boolean, error?: string }} Result
|
|
389
|
+
*/
|
|
390
|
+
function setPendingDeactivation(licenseKey, baseDir) {
|
|
391
|
+
try {
|
|
392
|
+
ensureAioxDir(baseDir);
|
|
393
|
+
|
|
394
|
+
const pendingPath = getPendingDeactivationPath(baseDir);
|
|
395
|
+
const machineId = generateMachineId();
|
|
396
|
+
|
|
397
|
+
const pendingData = {
|
|
398
|
+
licenseKey,
|
|
399
|
+
machineId,
|
|
400
|
+
deactivatedAt: new Date().toISOString(),
|
|
401
|
+
synced: false,
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
fs.writeFileSync(pendingPath, JSON.stringify(pendingData, null, 2), 'utf8');
|
|
405
|
+
|
|
406
|
+
return { success: true };
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return { success: false, error: error.message };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check if there's a pending deactivation to sync.
|
|
414
|
+
*
|
|
415
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
416
|
+
* @returns {{ pending: boolean, data?: object }} Pending status and data
|
|
417
|
+
*/
|
|
418
|
+
function hasPendingDeactivation(baseDir) {
|
|
419
|
+
try {
|
|
420
|
+
const pendingPath = getPendingDeactivationPath(baseDir);
|
|
421
|
+
|
|
422
|
+
if (!fs.existsSync(pendingPath)) {
|
|
423
|
+
return { pending: false };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const content = fs.readFileSync(pendingPath, 'utf8');
|
|
427
|
+
const data = JSON.parse(content);
|
|
428
|
+
|
|
429
|
+
if (data.synced) {
|
|
430
|
+
return { pending: false };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { pending: true, data };
|
|
434
|
+
} catch {
|
|
435
|
+
return { pending: false };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Mark pending deactivation as synced.
|
|
441
|
+
*
|
|
442
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
443
|
+
* @returns {{ success: boolean, error?: string }} Result
|
|
444
|
+
*/
|
|
445
|
+
function markPendingDeactivationSynced(baseDir) {
|
|
446
|
+
try {
|
|
447
|
+
const pendingPath = getPendingDeactivationPath(baseDir);
|
|
448
|
+
|
|
449
|
+
if (!fs.existsSync(pendingPath)) {
|
|
450
|
+
return { success: true };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const content = fs.readFileSync(pendingPath, 'utf8');
|
|
454
|
+
const data = JSON.parse(content);
|
|
455
|
+
|
|
456
|
+
data.synced = true;
|
|
457
|
+
data.syncedAt = new Date().toISOString();
|
|
458
|
+
|
|
459
|
+
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), 'utf8');
|
|
460
|
+
|
|
461
|
+
return { success: true };
|
|
462
|
+
} catch (error) {
|
|
463
|
+
return { success: false, error: error.message };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Clear the pending deactivation file.
|
|
469
|
+
*
|
|
470
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
471
|
+
* @returns {{ success: boolean, error?: string }} Result
|
|
472
|
+
*/
|
|
473
|
+
function clearPendingDeactivation(baseDir) {
|
|
474
|
+
try {
|
|
475
|
+
const pendingPath = getPendingDeactivationPath(baseDir);
|
|
476
|
+
|
|
477
|
+
if (fs.existsSync(pendingPath)) {
|
|
478
|
+
fs.unlinkSync(pendingPath);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { success: true };
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return { success: false, error: error.message };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Check if the cache file exists.
|
|
489
|
+
*
|
|
490
|
+
* @param {string} [baseDir] - Optional base directory override
|
|
491
|
+
* @returns {boolean} true if cache file exists
|
|
492
|
+
*/
|
|
493
|
+
function cacheExists(baseDir) {
|
|
494
|
+
return fs.existsSync(getCachePath(baseDir));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = {
|
|
498
|
+
// Core cache operations
|
|
499
|
+
writeLicenseCache,
|
|
500
|
+
readLicenseCache,
|
|
501
|
+
deleteLicenseCache,
|
|
502
|
+
|
|
503
|
+
// Expiry checks
|
|
504
|
+
isExpired,
|
|
505
|
+
isInGracePeriod,
|
|
506
|
+
getDaysRemaining,
|
|
507
|
+
getExpiryDate,
|
|
508
|
+
getLicenseState,
|
|
509
|
+
|
|
510
|
+
// Pending deactivation (offline scenario)
|
|
511
|
+
setPendingDeactivation,
|
|
512
|
+
hasPendingDeactivation,
|
|
513
|
+
markPendingDeactivationSynced,
|
|
514
|
+
clearPendingDeactivation,
|
|
515
|
+
|
|
516
|
+
// Utilities
|
|
517
|
+
cacheExists,
|
|
518
|
+
getCachePath,
|
|
519
|
+
getAioxDir,
|
|
520
|
+
|
|
521
|
+
// Exported for testing
|
|
522
|
+
_CONFIG: CONFIG,
|
|
523
|
+
};
|