@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.
- package/LICENSE +16 -0
- package/README.md +328 -0
- package/dist/aggregation/NativeAggregationEngine.d.ts +118 -0
- package/dist/aggregation/NativeAggregationEngine.js +186 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +236 -0
- package/dist/graph/GraphAccelerationAdapter.d.ts +153 -0
- package/dist/graph/GraphAccelerationAdapter.js +326 -0
- package/dist/graph/NativeGraphAdjacencyIndex.d.ts +286 -0
- package/dist/graph/NativeGraphAdjacencyIndex.js +944 -0
- package/dist/hnsw/AdaptiveDiskAnnModeSelector.d.ts +187 -0
- package/dist/hnsw/AdaptiveDiskAnnModeSelector.js +313 -0
- package/dist/hnsw/NativeDiskAnnWrapper.d.ts +489 -0
- package/dist/hnsw/NativeDiskAnnWrapper.js +1456 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +29 -0
- package/dist/legacyLayoutGuard.d.ts +93 -0
- package/dist/legacyLayoutGuard.js +237 -0
- package/dist/license/constants.d.ts +30 -0
- package/dist/license/constants.js +30 -0
- package/dist/license/onlineValidator.d.ts +43 -0
- package/dist/license/onlineValidator.js +225 -0
- package/dist/license/types.d.ts +39 -0
- package/dist/license/types.js +5 -0
- package/dist/license.d.ts +64 -0
- package/dist/license.js +165 -0
- package/dist/migration/MigrationCoordinator.d.ts +171 -0
- package/dist/migration/MigrationCoordinator.js +249 -0
- package/dist/migration/indexEpochGuard.d.ts +163 -0
- package/dist/migration/indexEpochGuard.js +202 -0
- package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
- package/dist/native/NativeEmbeddingEngine.js +318 -0
- package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
- package/dist/native/NativeRoaringBitmap32.js +221 -0
- package/dist/native/ffi.d.ts +20 -0
- package/dist/native/ffi.js +48 -0
- package/dist/native/idMapperTestSupport.d.ts +67 -0
- package/dist/native/idMapperTestSupport.js +112 -0
- package/dist/native/index.d.ts +49 -0
- package/dist/native/index.js +87 -0
- package/dist/native/napi.d.ts +21 -0
- package/dist/native/napi.js +88 -0
- package/dist/native/types.d.ts +1298 -0
- package/dist/native/types.js +16 -0
- package/dist/plugin.d.ts +50 -0
- package/dist/plugin.js +388 -0
- package/dist/providerContracts.d.ts +277 -0
- package/dist/providerContracts.js +38 -0
- package/dist/resource/OsMemoryProbe.d.ts +175 -0
- package/dist/resource/OsMemoryProbe.js +206 -0
- package/dist/resource/ResourceManager.d.ts +491 -0
- package/dist/resource/ResourceManager.js +960 -0
- package/dist/utils/ColumnManifest.d.ts +97 -0
- package/dist/utils/ColumnManifest.js +129 -0
- package/dist/utils/NativeColumnStore.d.ts +284 -0
- package/dist/utils/NativeColumnStore.js +685 -0
- package/dist/utils/NativeMetadataIndex.d.ts +882 -0
- package/dist/utils/NativeMetadataIndex.js +2631 -0
- package/dist/utils/NativeUnifiedCache.d.ts +87 -0
- package/dist/utils/NativeUnifiedCache.js +273 -0
- package/dist/utils/binaryIdMapperFactory.d.ts +59 -0
- package/dist/utils/binaryIdMapperFactory.js +94 -0
- package/dist/utils/collation.d.ts +30 -0
- package/dist/utils/collation.js +40 -0
- package/dist/utils/columnStoreTypes.d.ts +161 -0
- package/dist/utils/columnStoreTypes.js +29 -0
- package/dist/utils/nativeBinaryEntityIdMapper.d.ts +211 -0
- package/dist/utils/nativeBinaryEntityIdMapper.js +381 -0
- package/dist/utils/nativeEntityIdMapper.d.ts +111 -0
- package/dist/utils/nativeEntityIdMapper.js +170 -0
- package/docs/ADR-002-diskann-100-percent-rust.md +294 -0
- package/docs/ADR-003-semantic-time-travel.md +100 -0
- package/docs/aggregation.md +251 -0
- package/docs/comparison.md +162 -0
- package/docs/deployment-limits.md +87 -0
- package/docs/diskann.md +184 -0
- package/docs/migration-3.0.md +396 -0
- package/docs/performance-budget.md +274 -0
- package/docs/performance.md +117 -0
- package/docs/scaling.md +439 -0
- package/docs/snapshot-safety.md +246 -0
- package/docs/u64-id-space.md +214 -0
- package/docs/verification-report.md +670 -0
- package/native/brainy-native.node +0 -0
- package/native/index.d.ts +3916 -0
- package/package.json +92 -0
- 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,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
|
package/dist/license.js
ADDED
|
@@ -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
|