@soulcraft/cor 3.0.0-rc.1

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 (87) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +328 -0
  3. package/dist/aggregation/NativeAggregationEngine.d.ts +118 -0
  4. package/dist/aggregation/NativeAggregationEngine.js +186 -0
  5. package/dist/cli.d.ts +12 -0
  6. package/dist/cli.js +236 -0
  7. package/dist/graph/GraphAccelerationAdapter.d.ts +153 -0
  8. package/dist/graph/GraphAccelerationAdapter.js +326 -0
  9. package/dist/graph/NativeGraphAdjacencyIndex.d.ts +286 -0
  10. package/dist/graph/NativeGraphAdjacencyIndex.js +944 -0
  11. package/dist/hnsw/AdaptiveDiskAnnModeSelector.d.ts +187 -0
  12. package/dist/hnsw/AdaptiveDiskAnnModeSelector.js +313 -0
  13. package/dist/hnsw/NativeDiskAnnWrapper.d.ts +489 -0
  14. package/dist/hnsw/NativeDiskAnnWrapper.js +1456 -0
  15. package/dist/index.d.ts +28 -0
  16. package/dist/index.js +29 -0
  17. package/dist/legacyLayoutGuard.d.ts +93 -0
  18. package/dist/legacyLayoutGuard.js +237 -0
  19. package/dist/license/constants.d.ts +30 -0
  20. package/dist/license/constants.js +30 -0
  21. package/dist/license/onlineValidator.d.ts +43 -0
  22. package/dist/license/onlineValidator.js +225 -0
  23. package/dist/license/types.d.ts +39 -0
  24. package/dist/license/types.js +5 -0
  25. package/dist/license.d.ts +64 -0
  26. package/dist/license.js +165 -0
  27. package/dist/migration/MigrationCoordinator.d.ts +171 -0
  28. package/dist/migration/MigrationCoordinator.js +249 -0
  29. package/dist/migration/indexEpochGuard.d.ts +163 -0
  30. package/dist/migration/indexEpochGuard.js +202 -0
  31. package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
  32. package/dist/native/NativeEmbeddingEngine.js +318 -0
  33. package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
  34. package/dist/native/NativeRoaringBitmap32.js +221 -0
  35. package/dist/native/ffi.d.ts +20 -0
  36. package/dist/native/ffi.js +48 -0
  37. package/dist/native/idMapperTestSupport.d.ts +67 -0
  38. package/dist/native/idMapperTestSupport.js +112 -0
  39. package/dist/native/index.d.ts +49 -0
  40. package/dist/native/index.js +87 -0
  41. package/dist/native/napi.d.ts +21 -0
  42. package/dist/native/napi.js +88 -0
  43. package/dist/native/types.d.ts +1298 -0
  44. package/dist/native/types.js +16 -0
  45. package/dist/plugin.d.ts +50 -0
  46. package/dist/plugin.js +388 -0
  47. package/dist/providerContracts.d.ts +277 -0
  48. package/dist/providerContracts.js +38 -0
  49. package/dist/resource/OsMemoryProbe.d.ts +175 -0
  50. package/dist/resource/OsMemoryProbe.js +206 -0
  51. package/dist/resource/ResourceManager.d.ts +491 -0
  52. package/dist/resource/ResourceManager.js +960 -0
  53. package/dist/utils/ColumnManifest.d.ts +97 -0
  54. package/dist/utils/ColumnManifest.js +129 -0
  55. package/dist/utils/NativeColumnStore.d.ts +284 -0
  56. package/dist/utils/NativeColumnStore.js +685 -0
  57. package/dist/utils/NativeMetadataIndex.d.ts +882 -0
  58. package/dist/utils/NativeMetadataIndex.js +2631 -0
  59. package/dist/utils/NativeUnifiedCache.d.ts +87 -0
  60. package/dist/utils/NativeUnifiedCache.js +273 -0
  61. package/dist/utils/binaryIdMapperFactory.d.ts +59 -0
  62. package/dist/utils/binaryIdMapperFactory.js +94 -0
  63. package/dist/utils/collation.d.ts +30 -0
  64. package/dist/utils/collation.js +40 -0
  65. package/dist/utils/columnStoreTypes.d.ts +161 -0
  66. package/dist/utils/columnStoreTypes.js +29 -0
  67. package/dist/utils/nativeBinaryEntityIdMapper.d.ts +211 -0
  68. package/dist/utils/nativeBinaryEntityIdMapper.js +381 -0
  69. package/dist/utils/nativeEntityIdMapper.d.ts +111 -0
  70. package/dist/utils/nativeEntityIdMapper.js +170 -0
  71. package/docs/ADR-002-diskann-100-percent-rust.md +294 -0
  72. package/docs/ADR-003-semantic-time-travel.md +100 -0
  73. package/docs/aggregation.md +251 -0
  74. package/docs/comparison.md +162 -0
  75. package/docs/deployment-limits.md +87 -0
  76. package/docs/diskann.md +184 -0
  77. package/docs/migration-3.0.md +396 -0
  78. package/docs/performance-budget.md +274 -0
  79. package/docs/performance.md +117 -0
  80. package/docs/scaling.md +439 -0
  81. package/docs/snapshot-safety.md +246 -0
  82. package/docs/u64-id-space.md +214 -0
  83. package/docs/verification-report.md +670 -0
  84. package/native/brainy-native.node +0 -0
  85. package/native/index.d.ts +3916 -0
  86. package/package.json +92 -0
  87. package/scripts/migrate-cortex-2x-to-3x.mjs +970 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Online license heartbeat (v2)
3
+ *
4
+ * Background check against Portal API for:
5
+ * - Status-based subscription state (active, past_due, canceled, etc.)
6
+ * - Key renewal (subscription renewed, fresh JWT cached in memory)
7
+ * - Server-driven check intervals via nextCheckSeconds
8
+ *
9
+ * No signature verification here — that's in Rust.
10
+ * Default 24h heartbeat interval, 14-day grace period.
11
+ */
12
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { API_URL, CACHE_DIR, HEARTBEAT_FILE, GRACE_PERIOD_DAYS, DEFAULT_CHECK_INTERVAL_MS, ONLINE_VALIDATION_TIMEOUT_MS, } from './constants.js';
15
+ function getDefaultCacheDir() {
16
+ return join(process.cwd(), CACHE_DIR);
17
+ }
18
+ function getDefaultCachePath() {
19
+ return join(process.cwd(), CACHE_DIR, HEARTBEAT_FILE);
20
+ }
21
+ /**
22
+ * Read the heartbeat cache from disk. Returns null if not found or unreadable.
23
+ */
24
+ export async function readHeartbeatCache() {
25
+ return readHeartbeatCacheAt(getDefaultCachePath());
26
+ }
27
+ /**
28
+ * Check if a grace period start timestamp is expired (> GRACE_PERIOD_DAYS old).
29
+ */
30
+ export function isGracePeriodExpired(gracePeriodStart) {
31
+ const start = new Date(gracePeriodStart);
32
+ if (isNaN(start.getTime()))
33
+ return false;
34
+ const elapsedMs = Date.now() - start.getTime();
35
+ return elapsedMs >= GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1000;
36
+ }
37
+ /**
38
+ * Perform background heartbeat check.
39
+ *
40
+ * Call this after successful Rust validation. Non-blocking (fire and forget).
41
+ * Returns a warning string if the user should be notified.
42
+ * Returns degrade: true if the license is suspended or the grace period has expired.
43
+ *
44
+ * @param token - License key token
45
+ * @param options.forceCheck - Skip interval check, always call API (use at process startup)
46
+ */
47
+ export async function performHeartbeat(token, options) {
48
+ const cachePath = getDefaultCachePath();
49
+ const cacheDir = getDefaultCacheDir();
50
+ const cache = await readHeartbeatCacheAt(cachePath);
51
+ const forceCheck = options?.forceCheck ?? false;
52
+ // Old cache format (has revoked boolean, no status field) is treated as stale
53
+ const isDue = forceCheck || cache === null || !cache.status || isHeartbeatDue(cache);
54
+ if (!isDue) {
55
+ // Not due for a network check — return cached grace state
56
+ if (cache?.status === 'suspended') {
57
+ return { degrade: true };
58
+ }
59
+ if (cache?.gracePeriodStart) {
60
+ return checkGracePeriod(cache.gracePeriodStart, cache.status);
61
+ }
62
+ return {};
63
+ }
64
+ // Perform the network check
65
+ try {
66
+ const response = await callApi(token);
67
+ return await handleResponse(response, cacheDir, cachePath, cache);
68
+ }
69
+ catch {
70
+ // Network error — use cached state, don't block
71
+ if (cache?.status === 'suspended') {
72
+ return { degrade: true };
73
+ }
74
+ if (cache?.gracePeriodStart) {
75
+ return checkGracePeriod(cache.gracePeriodStart, cache.status);
76
+ }
77
+ return {};
78
+ }
79
+ }
80
+ function isHeartbeatDue(cache) {
81
+ // Use server-driven next check time if available
82
+ if (cache.nextCheckAt) {
83
+ const nextCheck = new Date(cache.nextCheckAt);
84
+ if (!isNaN(nextCheck.getTime())) {
85
+ return Date.now() >= nextCheck.getTime();
86
+ }
87
+ }
88
+ // Fallback: DEFAULT_CHECK_INTERVAL_MS from lastCheck
89
+ const lastCheck = new Date(cache.lastCheck);
90
+ if (isNaN(lastCheck.getTime()))
91
+ return true;
92
+ return Date.now() - lastCheck.getTime() >= DEFAULT_CHECK_INTERVAL_MS;
93
+ }
94
+ async function callApi(token) {
95
+ const controller = new AbortController();
96
+ const timeout = setTimeout(() => controller.abort(), ONLINE_VALIDATION_TIMEOUT_MS);
97
+ try {
98
+ const response = await fetch(API_URL, {
99
+ method: 'POST',
100
+ headers: { 'Content-Type': 'application/json' },
101
+ body: JSON.stringify({ licenseKey: token }),
102
+ signal: controller.signal,
103
+ });
104
+ if (!response.ok) {
105
+ throw new Error(`API returned ${response.status}`);
106
+ }
107
+ return (await response.json());
108
+ }
109
+ finally {
110
+ clearTimeout(timeout);
111
+ }
112
+ }
113
+ async function handleResponse(response, cacheDir, cachePath, existingCache) {
114
+ // 1. Cache renewed key in memory FIRST — before any status handling
115
+ if (response.renewedKey) {
116
+ const { updateCachedToken } = await import('../license.js');
117
+ updateCachedToken(response.renewedKey);
118
+ }
119
+ const now = new Date().toISOString();
120
+ const nextCheckAt = response.nextCheckSeconds
121
+ ? new Date(Date.now() + response.nextCheckSeconds * 1000).toISOString()
122
+ : undefined;
123
+ const status = response.status;
124
+ // 2. Dispatch on status
125
+ switch (status) {
126
+ case 'active': {
127
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status });
128
+ return {};
129
+ }
130
+ case 'past_due': {
131
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status });
132
+ return {
133
+ warning: response.warning ??
134
+ 'Subscription payment is past due. Please update your payment method at soulcraft.com/account',
135
+ };
136
+ }
137
+ case 'key_revoked': {
138
+ const gracePeriodStart = existingCache?.gracePeriodStart ?? now;
139
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status, gracePeriodStart });
140
+ return checkGracePeriod(gracePeriodStart, status);
141
+ }
142
+ case 'canceled': {
143
+ const gracePeriodStart = existingCache?.gracePeriodStart ?? now;
144
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status, gracePeriodStart });
145
+ return checkGracePeriod(gracePeriodStart, status);
146
+ }
147
+ case 'expired': {
148
+ const gracePeriodStart = existingCache?.gracePeriodStart ?? now;
149
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status, gracePeriodStart });
150
+ return checkGracePeriod(gracePeriodStart, status);
151
+ }
152
+ case 'not_found': {
153
+ const gracePeriodStart = existingCache?.gracePeriodStart ?? now;
154
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status, gracePeriodStart });
155
+ return checkGracePeriod(gracePeriodStart, status);
156
+ }
157
+ case 'suspended': {
158
+ // Immediate degrade — no grace period
159
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status });
160
+ return {
161
+ degrade: true,
162
+ warning: 'License suspended. Compute acceleration disabled. Contact support at soulcraft.com/support',
163
+ };
164
+ }
165
+ default: {
166
+ // Unknown status — treat as active if valid, else start grace period
167
+ if (response.valid) {
168
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status: status ?? 'active' });
169
+ return {};
170
+ }
171
+ const gracePeriodStart = existingCache?.gracePeriodStart ?? now;
172
+ await writeHeartbeatCache(cachePath, { lastCheck: now, nextCheckAt, status, gracePeriodStart });
173
+ return checkGracePeriod(gracePeriodStart, status);
174
+ }
175
+ }
176
+ }
177
+ function checkGracePeriod(gracePeriodStart, status) {
178
+ const start = new Date(gracePeriodStart);
179
+ if (isNaN(start.getTime())) {
180
+ return { warning: 'License validation failed. Please check your license at soulcraft.com/account' };
181
+ }
182
+ const elapsed = Date.now() - start.getTime();
183
+ const elapsedDays = elapsed / (1000 * 60 * 60 * 24);
184
+ const daysRemaining = Math.ceil(GRACE_PERIOD_DAYS - elapsedDays);
185
+ if (daysRemaining <= 0) {
186
+ return { degrade: true };
187
+ }
188
+ const dayStr = daysRemaining === 1 ? 'day' : 'days';
189
+ const actionMsg = statusToActionMessage(status);
190
+ return { warning: `${actionMsg} Grace period: ${daysRemaining} ${dayStr} remaining.` };
191
+ }
192
+ function statusToActionMessage(status) {
193
+ switch (status) {
194
+ case 'key_revoked':
195
+ return 'License key revoked. Get a new key at soulcraft.com/account.';
196
+ case 'canceled':
197
+ return 'Subscription canceled. Renew at soulcraft.com/account.';
198
+ case 'expired':
199
+ return 'Subscription expired. Renew at soulcraft.com/account.';
200
+ case 'not_found':
201
+ return 'License key not found. Your key may be corrupted. Check soulcraft.com/account.';
202
+ default:
203
+ return 'License lapsed. Renew at soulcraft.com/account.';
204
+ }
205
+ }
206
+ async function readHeartbeatCacheAt(path) {
207
+ try {
208
+ const content = await readFile(path, 'utf-8');
209
+ return JSON.parse(content);
210
+ }
211
+ catch {
212
+ return null;
213
+ }
214
+ }
215
+ async function writeHeartbeatCache(path, cache) {
216
+ try {
217
+ const dir = join(path, '..');
218
+ await mkdir(dir, { recursive: true });
219
+ await writeFile(path, JSON.stringify(cache, null, 2), 'utf-8');
220
+ }
221
+ catch {
222
+ // Silently fail if filesystem is read-only
223
+ }
224
+ }
225
+ //# sourceMappingURL=onlineValidator.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared types for license validation (v2)
3
+ */
4
+ /** Result of license validation */
5
+ export interface LicenseResult {
6
+ valid: boolean;
7
+ tier: 'free' | 'pro' | 'enterprise' | 'internal';
8
+ email: string;
9
+ expiresAt: number;
10
+ error?: string;
11
+ }
12
+ /** License details from Portal validation response */
13
+ export interface OnlineValidationLicense {
14
+ tier: string;
15
+ product: string;
16
+ email: string;
17
+ expiresAt: string | null;
18
+ }
19
+ /** Response from Portal validation API (heartbeat) */
20
+ export interface OnlineValidationResponse {
21
+ /** Whether the license key is valid */
22
+ valid: boolean;
23
+ /**
24
+ * License status from Portal:
25
+ * "active" | "past_due" | "key_revoked" | "canceled" | "suspended" | "not_found" | "expired"
26
+ */
27
+ status?: string;
28
+ /** Error message if invalid */
29
+ error?: string;
30
+ /** Warning message (e.g. for past_due) */
31
+ warning?: string;
32
+ /** License details when valid */
33
+ license?: OnlineValidationLicense;
34
+ /** Renewed license key (if subscription renewed, write to disk) */
35
+ renewedKey?: string;
36
+ /** Server-suggested seconds until next check */
37
+ nextCheckSeconds?: number;
38
+ }
39
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared types for license validation (v2)
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * License validation for @soulcraft/cor v2
3
+ *
4
+ * Sources, in precedence order:
5
+ * 1. COR_LICENSE_KEY env var (legacy CORTEX_LICENSE_KEY honored) — lets a
6
+ * deploy inject the key without writing a file: CI, immutable container
7
+ * images, serverless, and secrets managers (Vault / GCP Secret Manager).
8
+ * 2. .soulcraft.json in the project root.
9
+ * Either source may hold a full JWT or a short activation code (CO-XXXX-XXXX);
10
+ * a short code is exchanged for a JWT via the API on first use and cached in
11
+ * memory for the process lifetime.
12
+ */
13
+ export interface LicenseResult {
14
+ valid: boolean;
15
+ tier: 'free' | 'pro' | 'enterprise' | 'internal';
16
+ email: string;
17
+ expiresAt: number;
18
+ error?: string;
19
+ }
20
+ /**
21
+ * Update the in-memory cached token. Called by the heartbeat when the server
22
+ * issues a renewedKey — no file write needed.
23
+ */
24
+ export declare function updateCachedToken(jwt: string): void;
25
+ /**
26
+ * Test whether a string is a short activation code (CX-XXXX-XXXX).
27
+ */
28
+ export declare function isShortCode(value: string): boolean;
29
+ /**
30
+ * Read the project config file (.soulcraft.json).
31
+ * Returns an empty object if the file doesn't exist or can't be parsed.
32
+ */
33
+ export declare function readConfig(): Promise<Record<string, string>>;
34
+ /**
35
+ * Write keys to .soulcraft.json, merging with any existing content.
36
+ * Uses an atomic tmp → rename write to avoid partial writes.
37
+ */
38
+ export declare function writeConfig(config: Record<string, string>): Promise<void>;
39
+ /**
40
+ * Exchange a short activation code for a JWT via the API.
41
+ * Returns the JWT string, or null on any error (network, invalid code, etc.).
42
+ */
43
+ export declare function exchangeShortCode(code: string): Promise<string | null>;
44
+ /**
45
+ * Read the license token, performing short code exchange if needed.
46
+ *
47
+ * Sources (in precedence order):
48
+ * 1. In-memory cache (set by updateCachedToken after heartbeat renewal)
49
+ * 2. COR_LICENSE_KEY env var (legacy CORTEX_LICENSE_KEY honored) — 12-factor
50
+ * injection for CI / immutable images / serverless / secrets managers
51
+ * 3. .soulcraft.json in process.cwd() (`cor` key, legacy `cortex` fallback)
52
+ *
53
+ * The value from either external source may be a short code (exchanged for a
54
+ * JWT) or a full JWT (used directly).
55
+ */
56
+ export declare function readLicenseToken(): Promise<string | null>;
57
+ /**
58
+ * Validate the Soulcraft Cor license.
59
+ *
60
+ * Returns a LicenseResult with tier info. Free tier is always valid (no key needed).
61
+ * Pro/Enterprise/Internal require a valid Ed25519-signed JWT key.
62
+ */
63
+ export declare function validateLicense(): Promise<LicenseResult>;
64
+ //# sourceMappingURL=license.d.ts.map
@@ -0,0 +1,165 @@
1
+ /**
2
+ * License validation for @soulcraft/cor v2
3
+ *
4
+ * Sources, in precedence order:
5
+ * 1. COR_LICENSE_KEY env var (legacy CORTEX_LICENSE_KEY honored) — lets a
6
+ * deploy inject the key without writing a file: CI, immutable container
7
+ * images, serverless, and secrets managers (Vault / GCP Secret Manager).
8
+ * 2. .soulcraft.json in the project root.
9
+ * Either source may hold a full JWT or a short activation code (CO-XXXX-XXXX);
10
+ * a short code is exchanged for a JWT via the API on first use and cached in
11
+ * memory for the process lifetime.
12
+ */
13
+ import { readFile, writeFile, rename } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { CONFIG_FILE, EXCHANGE_URL, SHORT_CODE_PATTERN, ONLINE_VALIDATION_TIMEOUT_MS, } from './license/constants.js';
16
+ const FREE = { valid: false, tier: 'free', email: '', expiresAt: 0 };
17
+ /** In-memory cache for the exchanged JWT (process lifetime) */
18
+ let cachedToken = null;
19
+ /**
20
+ * Update the in-memory cached token. Called by the heartbeat when the server
21
+ * issues a renewedKey — no file write needed.
22
+ */
23
+ export function updateCachedToken(jwt) {
24
+ cachedToken = jwt;
25
+ }
26
+ /**
27
+ * Test whether a string is a short activation code (CX-XXXX-XXXX).
28
+ */
29
+ export function isShortCode(value) {
30
+ return SHORT_CODE_PATTERN.test(value);
31
+ }
32
+ /**
33
+ * Read the project config file (.soulcraft.json).
34
+ * Returns an empty object if the file doesn't exist or can't be parsed.
35
+ */
36
+ export async function readConfig() {
37
+ try {
38
+ const content = await readFile(join(process.cwd(), CONFIG_FILE), 'utf-8');
39
+ const parsed = JSON.parse(content);
40
+ if (typeof parsed === 'object' && parsed !== null) {
41
+ return parsed;
42
+ }
43
+ }
44
+ catch {
45
+ // Missing or malformed config — treat as empty
46
+ }
47
+ return {};
48
+ }
49
+ /**
50
+ * Write keys to .soulcraft.json, merging with any existing content.
51
+ * Uses an atomic tmp → rename write to avoid partial writes.
52
+ */
53
+ export async function writeConfig(config) {
54
+ const configPath = join(process.cwd(), CONFIG_FILE);
55
+ const tmpPath = `${configPath}.tmp`;
56
+ const existing = await readConfig();
57
+ const merged = { ...existing, ...config };
58
+ await writeFile(tmpPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
59
+ await rename(tmpPath, configPath);
60
+ }
61
+ /**
62
+ * Exchange a short activation code for a JWT via the API.
63
+ * Returns the JWT string, or null on any error (network, invalid code, etc.).
64
+ */
65
+ export async function exchangeShortCode(code) {
66
+ const controller = new AbortController();
67
+ const timeout = setTimeout(() => controller.abort(), ONLINE_VALIDATION_TIMEOUT_MS);
68
+ // Allow tests to override the exchange URL via environment variable
69
+ // (COR_TEST_EXCHANGE_URL; legacy CORTEX_TEST_EXCHANGE_URL still honored).
70
+ const exchangeUrl = process.env.COR_TEST_EXCHANGE_URL ?? process.env.CORTEX_TEST_EXCHANGE_URL ?? EXCHANGE_URL;
71
+ try {
72
+ const response = await fetch(exchangeUrl, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ code }),
76
+ signal: controller.signal,
77
+ });
78
+ if (!response.ok)
79
+ return null;
80
+ const data = (await response.json());
81
+ return data.licenseKey ?? data.token ?? null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ /**
91
+ * Resolve a raw license value (from the env var or the config file) into a
92
+ * usable JWT: a short activation code is exchanged for a JWT (and cached); a
93
+ * full JWT is used directly (and cached). Returns null if a short-code
94
+ * exchange fails.
95
+ */
96
+ async function resolveTokenValue(value) {
97
+ if (isShortCode(value)) {
98
+ const jwt = await exchangeShortCode(value);
99
+ if (jwt) {
100
+ cachedToken = jwt;
101
+ return jwt;
102
+ }
103
+ // Exchange failed — no token available
104
+ return null;
105
+ }
106
+ // Direct JWT (or legacy sc_cortex_ token)
107
+ cachedToken = value;
108
+ return value;
109
+ }
110
+ /**
111
+ * Read the license token, performing short code exchange if needed.
112
+ *
113
+ * Sources (in precedence order):
114
+ * 1. In-memory cache (set by updateCachedToken after heartbeat renewal)
115
+ * 2. COR_LICENSE_KEY env var (legacy CORTEX_LICENSE_KEY honored) — 12-factor
116
+ * injection for CI / immutable images / serverless / secrets managers
117
+ * 3. .soulcraft.json in process.cwd() (`cor` key, legacy `cortex` fallback)
118
+ *
119
+ * The value from either external source may be a short code (exchanged for a
120
+ * JWT) or a full JWT (used directly).
121
+ */
122
+ export async function readLicenseToken() {
123
+ if (cachedToken)
124
+ return cachedToken;
125
+ // Env var wins over the file so a deployment can inject the key without
126
+ // writing .soulcraft.json. Trimmed because secrets managers and `$(cat …)`
127
+ // often append a trailing newline.
128
+ const envValue = (process.env.COR_LICENSE_KEY ?? process.env.CORTEX_LICENSE_KEY)?.trim();
129
+ if (envValue)
130
+ return resolveTokenValue(envValue);
131
+ const config = await readConfig();
132
+ // Dual-read during the cortex→cor rename: prefer the new `cor` key, fall back
133
+ // to the legacy `cortex` key so already-deployed .soulcraft.json keeps working.
134
+ const value = config.cor ?? config.cortex;
135
+ if (!value)
136
+ return null;
137
+ return resolveTokenValue(value);
138
+ }
139
+ /**
140
+ * Validate the Soulcraft Cor license.
141
+ *
142
+ * Returns a LicenseResult with tier info. Free tier is always valid (no key needed).
143
+ * Pro/Enterprise/Internal require a valid Ed25519-signed JWT key.
144
+ */
145
+ export async function validateLicense() {
146
+ const token = await readLicenseToken();
147
+ if (!token)
148
+ return FREE;
149
+ try {
150
+ const { loadNativeModule } = await import('./native/index.js');
151
+ const native = loadNativeModule();
152
+ const result = native.validateLicenseKey(token);
153
+ return {
154
+ valid: result.valid,
155
+ tier: result.tier,
156
+ email: result.email,
157
+ expiresAt: result.expiresAt,
158
+ error: result.error ?? undefined,
159
+ };
160
+ }
161
+ catch {
162
+ return { ...FREE, error: 'native module unavailable' };
163
+ }
164
+ }
165
+ //# sourceMappingURL=license.js.map
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @module migration/MigrationCoordinator
3
+ * @description The cor-side driver of the #18 whole-brain 7.x → 8.0 upgrade — a
4
+ * **coordinated migration lock**. When cor opens a LARGE brain whose derived-index
5
+ * epoch is stale (drifted or absent; see {@link module:migration/indexEpochGuard}),
6
+ * the correct-and-clean upgrade is a BLOCKING one: rebuild every derived index
7
+ * (metadata LSM, DiskANN vector, graph adjacency) from the canonical records to a
8
+ * known-good state BEFORE serving — never serve a half-built or stale index. That
9
+ * decision (David, 2026-07-01) replaced the earlier "no-freeze background swap":
10
+ * no-freeze's availability is illusory (it serves degraded/half-built results),
11
+ * and *unknown/halfway states are more dangerous than a clean block*.
12
+ *
13
+ * ## The lock, coordinated with Brainy
14
+ *
15
+ * cor sets a lock the instant it detects a large stale brain; Brainy holds every
16
+ * public read AND write behind that lock (its `isMigrating()` gate, rc.9) and maps
17
+ * the lock to an HTTP-503-style readiness signal, so nothing observes a partial
18
+ * result and no write reaches a half-built index. The rebuild runs asynchronously
19
+ * — the LOCK, not a synchronous init hang, is what serializes access — and when
20
+ * all three indexes are rebuilt-from-canonical + the version marker is stamped,
21
+ * cor clears the lock and Brainy releases the queued operations against a fully
22
+ * correct brain. One invariant: *the brain is not readable/writable until all
23
+ * three indexes are rebuilt + stamped.*
24
+ *
25
+ * Brainy feature-detects three optional provider methods; every cor derived-index
26
+ * provider delegates them to the single coordinator so the answer is consistent
27
+ * across all three: {@link MigrationCoordinator#isMigrating} (the lock signal),
28
+ * {@link MigrationCoordinator#whenMigrationComplete} (the no-poll wait Brainy
29
+ * `Promise.race`s against its bounded timeout), and
30
+ * {@link MigrationCoordinator#migrationStatus} (the progress cor owns, which Brainy
31
+ * relays verbatim into `getIndexStatus().migration`).
32
+ *
33
+ * ## Failure is a locked-with-error state, never a half-served one
34
+ *
35
+ * If a rebuild throws, the coordinator KEEPS the brain locked (so Brainy never
36
+ * releases reads onto a half-built index) and rejects {@link
37
+ * MigrationCoordinator#whenMigrationComplete} with a loud, actionable error — the
38
+ * same "blocking beats halfway" principle applied to the failure path. A restart
39
+ * re-detects the drift and re-runs the idempotent in-place rebuild, so a transient
40
+ * hiccup self-heals; genuinely-corrupt canonical data stays safely blocked with a
41
+ * pointer to the offline `scripts/migrate-cortex-2x-to-3x.mjs` escape hatch.
42
+ *
43
+ * The rebuild is IN-PLACE (no shadow directories, no serve-from-canonical, no
44
+ * concurrent-write reconciliation) precisely because everything is locked: no live
45
+ * index is read while it is being rebuilt.
46
+ */
47
+ import type { StorageAdapter } from '@soulcraft/brainy';
48
+ /** The three derived indexes cor rebuilds from canonical, in rebuild order. */
49
+ export type MigrationIndexKind = 'metadata' | 'vector' | 'graph';
50
+ /**
51
+ * @description Structured migration progress. cor owns every field; Brainy relays
52
+ * it verbatim into `getIndexStatus().migration` (Brainy adds `startedAt`/`elapsedMs`
53
+ * from when it first observed the lock). Every field is optional so a coarse
54
+ * "which index is rebuilding" answer is valid before per-entity counts arrive.
55
+ */
56
+ export interface MigrationStatus {
57
+ /** Coarse stage, e.g. `'rebuilding'` / `'starting'` / `'failed'`. */
58
+ phase?: string;
59
+ /** Which derived index is rebuilding right now. */
60
+ index?: MigrationIndexKind;
61
+ /** 0–100 completion of the current index (omitted when the total is unknown). */
62
+ percent?: number;
63
+ /** Entities rebuilt into the current index so far. */
64
+ entitiesDone?: number;
65
+ /** Total entities the current index rebuild will process. */
66
+ entitiesTotal?: number;
67
+ }
68
+ /**
69
+ * @description The single capability the coordinator needs from each derived-index
70
+ * provider: a from-canonical rebuild. cor's three providers already rebuild from
71
+ * canonical (`#74` cursor-paginated / OOM-safe at any scale) with a no-argument
72
+ * `rebuild()` (the vector wrapper's optional-options overload is assignable to
73
+ * this). Per-entity progress — `migrationStatus().percent`/`entitiesDone` — is an
74
+ * additive follow-up that threads a callback through the rebuild cursors; it does
75
+ * NOT change this contract (those status fields are already optional), so the
76
+ * coordinator reports at the phase (per-index) granularity today.
77
+ */
78
+ export interface MigratableProvider {
79
+ rebuild(): Promise<void>;
80
+ }
81
+ /** Lazy resolvers for the three provider instances (they are constructed by
82
+ * Brainy's provider factories after the coordinator, so they are read on demand —
83
+ * the same lazy-capture pattern the plugin uses for the id-mapper resolver). A
84
+ * resolver returning `undefined` (e.g. a provider a cloud adapter never engages)
85
+ * is skipped, not an error. */
86
+ export interface MigrationProviders {
87
+ metadata: () => MigratableProvider | undefined;
88
+ vector: () => MigratableProvider | undefined;
89
+ graph: () => MigratableProvider | undefined;
90
+ }
91
+ /**
92
+ * @description Drives the #18 coordinated blocking migration and answers the three
93
+ * Brainy-feature-detected lock methods. One instance per brain, created in the
94
+ * plugin's `activate` closure and shared by all three derived-index providers.
95
+ *
96
+ * @example
97
+ * const coordinator = new MigrationCoordinator(storage, {
98
+ * metadata: () => metadataIndexInstance,
99
+ * vector: () => vectorInstance,
100
+ * graph: () => graphEngine,
101
+ * })
102
+ * // ...in metadata init(), on a large stale brain:
103
+ * coordinator.start() // flips isMigrating() true synchronously, rebuilds async
104
+ */
105
+ export declare class MigrationCoordinator {
106
+ private readonly storage;
107
+ private readonly providers;
108
+ /**
109
+ * `idle` before/without a migration; `migrating` while rebuilding; `done` once
110
+ * stamped; `failed` when a rebuild threw. `failed` reads as LOCKED (see
111
+ * {@link isMigrating}) so Brainy never releases reads onto a half-built index.
112
+ */
113
+ private state;
114
+ private status;
115
+ private completePromise;
116
+ private resolveComplete;
117
+ private rejectComplete;
118
+ private failure;
119
+ /**
120
+ * @param storage - The brain's storage adapter — passed to `writeBrainFormat`
121
+ * as the final stamp step (Brainy's own `dataFormat`/`indexEpoch` constants;
122
+ * cor never authors the format string).
123
+ * @param providers - Lazy resolvers for the three derived-index providers.
124
+ */
125
+ constructor(storage: StorageAdapter, providers: MigrationProviders);
126
+ /**
127
+ * @description The lock signal Brainy gates every public read AND write on.
128
+ * `true` while migrating OR after a failure (a failed migration stays LOCKED so
129
+ * Brainy never serves the half-built index — a restart re-runs the idempotent
130
+ * rebuild).
131
+ * @returns `true` when the brain must not be read from or written to.
132
+ */
133
+ isMigrating(): boolean;
134
+ /**
135
+ * @description Resolves when the migration completes (lock clears), or REJECTS
136
+ * with the loud failure error if it failed. Used internally by {@link start} (the
137
+ * already-migrating idempotency path) and by tests as the completion signal.
138
+ *
139
+ * Brainy does NOT call this: rc.9 dropped the provider-level `whenMigrationComplete`
140
+ * hook and polls `isMigrating()` at 250 ms instead (the event-driven variant
141
+ * busy-spun with mixed providers). `getIndexStatus().migrating` is the readiness
142
+ * signal, and a blocked op waits out the lock up to `migrationWaitTimeoutMs` before
143
+ * a retryable `MigrationInProgressError`.
144
+ * @returns A promise that settles when the lock is released or the migration fails.
145
+ */
146
+ whenMigrationComplete(): Promise<void>;
147
+ /**
148
+ * @description The structured progress Brainy relays into
149
+ * `getIndexStatus().migration`. `null` unless a migration is in flight.
150
+ * @returns The current {@link MigrationStatus}, or `null`.
151
+ */
152
+ migrationStatus(): MigrationStatus | null;
153
+ /**
154
+ * @description Kick the coordinated blocking rebuild. Flips {@link isMigrating}
155
+ * to `true` **synchronously** (so Brainy sees the lock the instant the triggering
156
+ * `init()` returns), then runs the rebuild asynchronously — the LOCK, not this
157
+ * promise, is what serializes reads/writes, so callers do NOT await this.
158
+ * Idempotent: calling it again while already migrating returns the in-flight
159
+ * completion promise without starting a second rebuild.
160
+ * @returns The completion promise (settles when the lock clears or the migration fails).
161
+ */
162
+ start(): Promise<void>;
163
+ /**
164
+ * The async body: rebuild metadata → vector → graph from canonical, verify by
165
+ * stamping (the marker certifies all three; Brainy withholds its own stamp while
166
+ * we are migrating), then clear the lock. Any throw leaves the brain LOCKED and
167
+ * rejects the completion promise.
168
+ */
169
+ private run;
170
+ }
171
+ //# sourceMappingURL=MigrationCoordinator.d.ts.map