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.
Files changed (91) hide show
  1. package/.aiox-core/data/entity-registry.yaml +5297 -1814
  2. package/.aiox-core/data/registry-update-log.jsonl +2 -0
  3. package/.aiox-core/development/templates/service-template/README.md.hbs +158 -158
  4. package/.aiox-core/development/templates/service-template/__tests__/index.test.ts.hbs +237 -237
  5. package/.aiox-core/development/templates/service-template/client.ts.hbs +403 -403
  6. package/.aiox-core/development/templates/service-template/errors.ts.hbs +182 -182
  7. package/.aiox-core/development/templates/service-template/index.ts.hbs +120 -120
  8. package/.aiox-core/development/templates/service-template/package.json.hbs +87 -87
  9. package/.aiox-core/development/templates/service-template/types.ts.hbs +145 -145
  10. package/.aiox-core/development/templates/squad-template/LICENSE +21 -21
  11. package/.aiox-core/infrastructure/scripts/tool-resolver.js +4 -4
  12. package/.aiox-core/infrastructure/templates/aiox-sync.yaml.template +182 -182
  13. package/.aiox-core/infrastructure/templates/coderabbit.yaml.template +279 -279
  14. package/.aiox-core/infrastructure/templates/github-workflows/ci.yml.template +169 -169
  15. package/.aiox-core/infrastructure/templates/github-workflows/pr-automation.yml.template +330 -330
  16. package/.aiox-core/infrastructure/templates/github-workflows/release.yml.template +196 -196
  17. package/.aiox-core/infrastructure/templates/gitignore/gitignore-aiox-base.tmpl +63 -63
  18. package/.aiox-core/infrastructure/templates/gitignore/gitignore-brownfield-merge.tmpl +18 -18
  19. package/.aiox-core/infrastructure/templates/gitignore/gitignore-node.tmpl +85 -85
  20. package/.aiox-core/infrastructure/templates/gitignore/gitignore-python.tmpl +145 -145
  21. package/.aiox-core/install-manifest.yaml +58 -58
  22. package/.aiox-core/local-config.yaml.template +71 -71
  23. package/.aiox-core/monitor/hooks/lib/__init__.py +1 -1
  24. package/.aiox-core/monitor/hooks/lib/enrich.py +58 -58
  25. package/.aiox-core/monitor/hooks/lib/send_event.py +47 -47
  26. package/.aiox-core/monitor/hooks/notification.py +29 -29
  27. package/.aiox-core/monitor/hooks/post_tool_use.py +45 -45
  28. package/.aiox-core/monitor/hooks/pre_compact.py +29 -29
  29. package/.aiox-core/monitor/hooks/pre_tool_use.py +40 -40
  30. package/.aiox-core/monitor/hooks/stop.py +29 -29
  31. package/.aiox-core/monitor/hooks/subagent_stop.py +29 -29
  32. package/.aiox-core/monitor/hooks/user_prompt_submit.py +38 -38
  33. package/.aiox-core/product/templates/adr.hbs +125 -125
  34. package/.aiox-core/product/templates/dbdr.hbs +241 -241
  35. package/.aiox-core/product/templates/engine/elicitation.js +2 -3
  36. package/.aiox-core/product/templates/epic.hbs +212 -212
  37. package/.aiox-core/product/templates/pmdr.hbs +186 -186
  38. package/.aiox-core/product/templates/prd-v2.0.hbs +216 -216
  39. package/.aiox-core/product/templates/prd.hbs +201 -201
  40. package/.aiox-core/product/templates/story.hbs +263 -263
  41. package/.aiox-core/product/templates/task.hbs +170 -170
  42. package/.aiox-core/product/templates/tmpl-comment-on-examples.sql +158 -158
  43. package/.aiox-core/product/templates/tmpl-migration-script.sql +91 -91
  44. package/.aiox-core/product/templates/tmpl-rls-granular-policies.sql +104 -104
  45. package/.aiox-core/product/templates/tmpl-rls-kiss-policy.sql +10 -10
  46. package/.aiox-core/product/templates/tmpl-rls-roles.sql +135 -135
  47. package/.aiox-core/product/templates/tmpl-rls-simple.sql +77 -77
  48. package/.aiox-core/product/templates/tmpl-rls-tenant.sql +152 -152
  49. package/.aiox-core/product/templates/tmpl-rollback-script.sql +77 -77
  50. package/.aiox-core/product/templates/tmpl-seed-data.sql +140 -140
  51. package/.aiox-core/product/templates/tmpl-smoke-test.sql +16 -16
  52. package/.aiox-core/product/templates/tmpl-staging-copy-merge.sql +139 -139
  53. package/.aiox-core/product/templates/tmpl-stored-proc.sql +140 -140
  54. package/.aiox-core/product/templates/tmpl-trigger.sql +152 -152
  55. package/.aiox-core/product/templates/tmpl-view-materialized.sql +133 -133
  56. package/.aiox-core/product/templates/tmpl-view.sql +177 -177
  57. package/.aiox-core/scripts/pm.sh +0 -0
  58. package/.claude/hooks/code-intel-pretool.cjs +107 -0
  59. package/.claude/hooks/enforce-architecture-first.py +196 -196
  60. package/.claude/hooks/mind-clone-governance.py +192 -192
  61. package/.claude/hooks/read-protection.py +151 -151
  62. package/.claude/hooks/slug-validation.py +176 -176
  63. package/.claude/hooks/sql-governance.py +182 -182
  64. package/.claude/hooks/write-path-validation.py +194 -194
  65. package/LICENSE +33 -33
  66. package/bin/aiox-graph.js +0 -0
  67. package/bin/aiox-minimal.js +0 -0
  68. package/bin/aiox.js +0 -0
  69. package/docs/guides/aios-workflows/README.md +247 -0
  70. package/docs/guides/aios-workflows/bob-orchestrator-workflow.md +1536 -0
  71. package/package.json +1 -1
  72. package/packages/aiox-install/bin/aiox-install.js +0 -0
  73. package/packages/aiox-install/bin/edmcp.js +0 -0
  74. package/packages/aiox-pro-cli/bin/aiox-pro.js +0 -0
  75. package/packages/installer/src/wizard/pro-setup.js +210 -123
  76. package/pro/README.md +66 -0
  77. package/pro/license/degradation.js +220 -0
  78. package/pro/license/errors.js +450 -0
  79. package/pro/license/feature-gate.js +354 -0
  80. package/pro/license/index.js +181 -0
  81. package/pro/license/license-api.js +679 -0
  82. package/pro/license/license-cache.js +523 -0
  83. package/pro/license/license-crypto.js +303 -0
  84. package/scripts/check-markdown-links.py +352 -352
  85. package/scripts/dashboard-parallel-dev.sh +0 -0
  86. package/scripts/dashboard-parallel-phase3.sh +0 -0
  87. package/scripts/dashboard-parallel-phase4.sh +0 -0
  88. package/scripts/glue/README.md +355 -0
  89. package/scripts/glue/compose-agent-prompt.cjs +362 -0
  90. package/scripts/install-monitor-hooks.sh +0 -0
  91. 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
+ };