@harperfast/harper 5.0.0-beta.6 → 5.0.0-beta.8
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/bin/cliOperations.js +3 -3
- package/bin/harper.js +1 -1
- package/bin/run.js +2 -2
- package/components/Application.ts +6 -3
- package/components/ApplicationScope.ts +1 -0
- package/components/componentLoader.ts +23 -12
- package/components/operations.js +13 -13
- package/components/operationsValidation.js +3 -3
- package/config/configUtils.js +20 -4
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +3 -2
- package/dist/bin/cliOperations.js +3 -3
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/harper.d.ts +1 -1
- package/dist/bin/harper.js +1 -1
- package/dist/bin/run.js +2 -2
- package/dist/bin/run.js.map +1 -1
- package/dist/components/Application.js +7 -2
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +1 -0
- package/dist/components/ApplicationScope.js +1 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/componentLoader.js +21 -10
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/operations.js +13 -13
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +3 -3
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/config/configUtils.d.ts +6 -0
- package/dist/config/configUtils.js +23 -3
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +3 -2
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +2 -1
- package/dist/resources/DatabaseTransaction.js +71 -35
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/Resource.d.ts +2 -0
- package/dist/resources/Resource.js +11 -4
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +1 -0
- package/dist/resources/RocksTransactionLogStore.js +8 -1
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -6
- package/dist/resources/Table.js +15 -10
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -0
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/databases.js +3 -2
- package/dist/resources/databases.js.map +1 -1
- package/dist/security/jsLoader.js +223 -116
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +30 -14
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +6 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +1 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +1 -0
- package/dist/utility/hdbTerms.js +1 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +24 -1
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/readLog.js +2 -2
- package/dist/utility/logging/readLog.js.map +1 -1
- package/dist/utility/npmUtilities.js +2 -2
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/validation/configValidator.js +18 -8
- package/dist/validation/configValidator.js.map +1 -1
- package/dist/validation/readLogValidator.js +2 -2
- package/dist/validation/readLogValidator.js.map +1 -1
- package/package.json +5 -5
- package/resources/DatabaseTransaction.ts +67 -32
- package/resources/Resource.ts +17 -6
- package/resources/RocksTransactionLogStore.ts +8 -1
- package/resources/Table.ts +29 -10
- package/resources/analytics/write.ts +6 -0
- package/resources/databases.ts +3 -2
- package/security/jsLoader.ts +258 -129
- package/server/REST.ts +32 -13
- package/server/http.ts +6 -3
- package/server/itc/serverHandlers.js +1 -1
- package/static/defaultConfig.yaml +1 -1
- package/studio/web/assets/{index-BckVDix4.js → index-CXQsBaYq.js} +5 -5
- package/studio/web/assets/{index-BckVDix4.js.map → index-CXQsBaYq.js.map} +1 -1
- package/studio/web/assets/{index.lazy-iG1_8dzm.js → index.lazy-C3Ejfvna.js} +2 -2
- package/studio/web/assets/{index.lazy-iG1_8dzm.js.map → index.lazy-C3Ejfvna.js.map} +1 -1
- package/studio/web/assets/{profile-CzjslUXv.js → profile-BbbbWJCN.js} +2 -2
- package/studio/web/assets/{profile-CzjslUXv.js.map → profile-BbbbWJCN.js.map} +1 -1
- package/studio/web/assets/{status-BP4TQJDR.js → status-CFe85l8C.js} +2 -2
- package/studio/web/assets/{status-BP4TQJDR.js.map → status-CFe85l8C.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/hdbTerms.ts +1 -0
- package/utility/logging/harper_logger.js +22 -1
- package/utility/logging/readLog.js +2 -2
- package/utility/npmUtilities.js +2 -2
- package/validation/configValidator.js +16 -8
- package/validation/readLogValidator.js +2 -2
package/security/jsLoader.ts
CHANGED
|
@@ -56,6 +56,9 @@ export async function scopedImport(filePath: string | URL, scope?: ApplicationSc
|
|
|
56
56
|
}
|
|
57
57
|
overridableProperty(Promise.prototype, 'then');
|
|
58
58
|
overridableProperty(Date, 'now');
|
|
59
|
+
for (let name of ['get', 'set', 'has', 'delete', 'clear', 'forEach', 'entries', 'keys', 'values']) {
|
|
60
|
+
overridableProperty(Map.prototype, name);
|
|
61
|
+
}
|
|
59
62
|
for (let Intrinsic of [
|
|
60
63
|
Object,
|
|
61
64
|
Array,
|
|
@@ -121,12 +124,13 @@ export async function scopedImport(filePath: string | URL, scope?: ApplicationSc
|
|
|
121
124
|
|
|
122
125
|
let amaro: typeof import('amaro') | undefined;
|
|
123
126
|
/**
|
|
124
|
-
* Strip TypeScript types using the amaro library (what Node.js uses internally)
|
|
125
|
-
* Falls back to regex-based stripping if amaro is not available
|
|
127
|
+
* Strip TypeScript types synchronously using the amaro library (what Node.js uses internally)
|
|
126
128
|
*/
|
|
127
|
-
|
|
129
|
+
function stripTypeScriptTypes(source: string): string {
|
|
128
130
|
// Use amaro - the library that Node.js uses internally for type stripping
|
|
129
|
-
|
|
131
|
+
if (!amaro) {
|
|
132
|
+
amaro = require('amaro');
|
|
133
|
+
}
|
|
130
134
|
return amaro.transformSync(source, { mode: 'strip-only' }).code;
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -146,12 +150,27 @@ function parseJsonModule(source: string, url: string): any {
|
|
|
146
150
|
* Load a module using Node's vm.Module API with (not really secure) sandboxing
|
|
147
151
|
*/
|
|
148
152
|
async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// we want to retain the same module caches across any loading with the application scope
|
|
154
|
+
let moduleCaches = scope.moduleCache as {
|
|
155
|
+
moduleCache: Map<string, SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule>>;
|
|
156
|
+
linkingPromises: Map<string, Promise<void>>;
|
|
157
|
+
cjsCache: Map<string, { exports: any }>;
|
|
158
|
+
contextObject: any;
|
|
159
|
+
context: any;
|
|
160
|
+
};
|
|
161
|
+
if (!moduleCaches) {
|
|
162
|
+
// if they haven't been initialized, do so now
|
|
163
|
+
const contextObject = getGlobalObject(scope, true);
|
|
164
|
+
moduleCaches = scope.moduleCache = {
|
|
165
|
+
moduleCache: new Map(),
|
|
166
|
+
linkingPromises: new Map(),
|
|
167
|
+
cjsCache: new Map(),
|
|
168
|
+
// Create a secure context with limited globals
|
|
169
|
+
contextObject,
|
|
170
|
+
context: createContext(contextObject),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const { moduleCache, linkingPromises, cjsCache, contextObject, context } = moduleCaches;
|
|
155
174
|
|
|
156
175
|
/**
|
|
157
176
|
* Resolve module specifier to absolute URL
|
|
@@ -165,6 +184,9 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
165
184
|
// block harper/* for now (reserving for potential future use)
|
|
166
185
|
throw new Error(`Module ${specifier} is not allowed, may only access the 'harper' module`);
|
|
167
186
|
}
|
|
187
|
+
if (parts[0] === 'file:') {
|
|
188
|
+
return specifier;
|
|
189
|
+
}
|
|
168
190
|
const resolved = createRequire(referrer).resolve(specifier);
|
|
169
191
|
if (isAbsolute(resolved)) {
|
|
170
192
|
return pathToFileURL(resolved).toString();
|
|
@@ -176,7 +198,16 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
176
198
|
* Load a CommonJS module in our private context
|
|
177
199
|
*/
|
|
178
200
|
function loadCJS(url: string, source: string): { exports: any } {
|
|
201
|
+
// Check cache first to handle circular dependencies
|
|
202
|
+
if (cjsCache.has(url)) {
|
|
203
|
+
return cjsCache.get(url)!;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Create module object and cache it immediately (before execution)
|
|
207
|
+
// This allows circular dependencies to get a reference to the incomplete module
|
|
179
208
|
const cjsModule = { exports: {} };
|
|
209
|
+
cjsCache.set(url, cjsModule);
|
|
210
|
+
|
|
180
211
|
if (url.endsWith('.json')) {
|
|
181
212
|
cjsModule.exports = parseJsonModule(source, url);
|
|
182
213
|
return cjsModule;
|
|
@@ -204,7 +235,7 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
204
235
|
filename: url,
|
|
205
236
|
async importModuleDynamically(specifier: string, script) {
|
|
206
237
|
const resolvedUrl = resolveModule(specifier, script.sourceURL);
|
|
207
|
-
const useContainment = specifier.startsWith('.') || scope.dependencyContainment;
|
|
238
|
+
const useContainment = specifier.startsWith('.') || scope.dependencyContainment !== false;
|
|
208
239
|
const dynamicModule = await loadModuleWithCache(resolvedUrl, useContainment);
|
|
209
240
|
return dynamicModule;
|
|
210
241
|
},
|
|
@@ -221,16 +252,18 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
221
252
|
}
|
|
222
253
|
function loadCJSModule(url: string, source: string, usePrivateGlobal: boolean): SyntheticModule {
|
|
223
254
|
const cjsModule = usePrivateGlobal ? loadCJS(url, source) : { exports: require(url) };
|
|
224
|
-
|
|
255
|
+
let exports = cjsModule.exports;
|
|
256
|
+
if (exports.default === undefined) {
|
|
257
|
+
// provide the default export for compatibility
|
|
258
|
+
exports = { default: exports, ...exports };
|
|
259
|
+
}
|
|
260
|
+
const exportNames = Object.keys(exports);
|
|
261
|
+
|
|
225
262
|
const synModule = new SyntheticModule(
|
|
226
|
-
exportNames
|
|
263
|
+
exportNames,
|
|
227
264
|
function () {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.setExport(key, cjsModule.exports[key]);
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
this.setExport('default', cjsModule.exports);
|
|
265
|
+
for (const key of exportNames) {
|
|
266
|
+
this.setExport(key, exports[key]);
|
|
234
267
|
}
|
|
235
268
|
},
|
|
236
269
|
{ identifier: url, context }
|
|
@@ -240,21 +273,78 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
/**
|
|
243
|
-
*
|
|
276
|
+
* Check if a package (or any of its dependencies) depends on harper
|
|
277
|
+
* Expects a file URL like: file:///path/to/node_modules/package-name/dist/index.js
|
|
278
|
+
*/
|
|
279
|
+
function packageDependsOnHarper(fileUrl: string): boolean {
|
|
280
|
+
try {
|
|
281
|
+
// Convert file:// URL to path
|
|
282
|
+
const filePath = fileURLToPath(fileUrl);
|
|
283
|
+
|
|
284
|
+
// Find the node_modules directory and package name
|
|
285
|
+
// Example: /path/to/node_modules/package-name/dist/index.js
|
|
286
|
+
// or: /path/to/node_modules/@scope/package-name/dist/index.js
|
|
287
|
+
const nodeModulesMarker = '/node_modules/';
|
|
288
|
+
const nodeModulesIndex = filePath.lastIndexOf(nodeModulesMarker);
|
|
289
|
+
if (nodeModulesIndex === -1) return false;
|
|
290
|
+
|
|
291
|
+
// Get the part after /node_modules/
|
|
292
|
+
const afterNodeModules = filePath.substring(nodeModulesIndex + nodeModulesMarker.length);
|
|
293
|
+
const parts = afterNodeModules.split('/');
|
|
294
|
+
|
|
295
|
+
// Handle scoped packages (@scope/package-name) vs regular packages (package-name)
|
|
296
|
+
const beforeNodeModules = filePath.substring(0, nodeModulesIndex);
|
|
297
|
+
const packageRoot = parts[0].startsWith('@')
|
|
298
|
+
? join(beforeNodeModules, 'node_modules', parts[0], parts[1])
|
|
299
|
+
: join(beforeNodeModules, 'node_modules', parts[0]);
|
|
300
|
+
|
|
301
|
+
// Read package.json from the package root
|
|
302
|
+
const packageJsonPath = join(packageRoot, 'package.json');
|
|
303
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
304
|
+
|
|
305
|
+
const deps = {
|
|
306
|
+
...packageJson.dependencies,
|
|
307
|
+
...packageJson.devDependencies,
|
|
308
|
+
...packageJson.peerDependencies,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Check if harper is a direct dependency
|
|
312
|
+
return Object.keys(deps).some((dep) => HARPER_MODULE_IDS.has(dep));
|
|
313
|
+
} catch {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Linker function for module resolution during instantiation.
|
|
320
|
+
* This is synchronous because Node's module.link() requires the linker
|
|
321
|
+
* to return modules synchronously.
|
|
244
322
|
*/
|
|
245
|
-
|
|
323
|
+
function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
|
|
246
324
|
const resolvedUrl = resolveModule(specifier, referencingModule.identifier);
|
|
247
325
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
326
|
+
// Determine if we should use VM containment for this module
|
|
327
|
+
let useContainment = specifier.startsWith('.'); // Always contain relative imports
|
|
328
|
+
|
|
329
|
+
if (!useContainment && scope.dependencyContainment !== false) {
|
|
330
|
+
// For npm packages, check if they depend on harper
|
|
331
|
+
if (resolvedUrl.startsWith('file://') && resolvedUrl.includes('node_modules')) {
|
|
332
|
+
useContainment = packageDependsOnHarper(resolvedUrl);
|
|
333
|
+
} else {
|
|
334
|
+
// Non-file URLs (bare specifiers) - use default behavior
|
|
335
|
+
useContainment = scope.dependencyContainment === true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Return the module
|
|
340
|
+
return getOrCreateModule(resolvedUrl, useContainment);
|
|
251
341
|
}
|
|
252
342
|
|
|
253
|
-
|
|
343
|
+
function getOrCreateModule(
|
|
254
344
|
url: string,
|
|
255
345
|
usePrivateGlobal: boolean
|
|
256
|
-
): Promise<SourceTextModule | SyntheticModule> {
|
|
257
|
-
// Check
|
|
346
|
+
): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
|
|
347
|
+
// Check if module is already created
|
|
258
348
|
if (moduleCache.has(url)) {
|
|
259
349
|
return moduleCache.get(url)!;
|
|
260
350
|
}
|
|
@@ -275,8 +365,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
275
365
|
// Only link/evaluate once per module
|
|
276
366
|
if (!linkingPromises.has(url)) {
|
|
277
367
|
const linkingPromise = (async () => {
|
|
278
|
-
|
|
279
|
-
|
|
368
|
+
// Check module status - only link if it's 'unlinked'
|
|
369
|
+
// Status can be: 'unlinked', 'linking', 'linked', 'evaluating', 'evaluated'
|
|
370
|
+
if (module.status === 'unlinked') {
|
|
371
|
+
await module.link(linker);
|
|
372
|
+
}
|
|
373
|
+
// Only evaluate if not already evaluated
|
|
374
|
+
if (module.status === 'linked') {
|
|
375
|
+
await module.evaluate();
|
|
376
|
+
}
|
|
280
377
|
})();
|
|
281
378
|
linkingPromises.set(url, linkingPromise);
|
|
282
379
|
}
|
|
@@ -287,107 +384,107 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
287
384
|
return module;
|
|
288
385
|
}
|
|
289
386
|
/**
|
|
290
|
-
* Create a
|
|
387
|
+
* Create a SyntheticModule from exported object
|
|
291
388
|
*/
|
|
292
|
-
|
|
293
|
-
|
|
389
|
+
function createSyntheticModule(url: string, exportedObject: any): SyntheticModule {
|
|
390
|
+
const exportNames = Object.keys(exportedObject);
|
|
391
|
+
return new SyntheticModule(
|
|
392
|
+
exportNames,
|
|
393
|
+
function () {
|
|
394
|
+
for (const key of exportNames) {
|
|
395
|
+
this.setExport(key, exportedObject[key]);
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{ identifier: url, context }
|
|
399
|
+
);
|
|
400
|
+
}
|
|
294
401
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Normalize imported module to ensure it has proper exports including default
|
|
404
|
+
*/
|
|
405
|
+
function normalizeImportedModule(importedModule: any): any {
|
|
406
|
+
const cjsModule = importedModule['module.exports'];
|
|
407
|
+
if (cjsModule) {
|
|
408
|
+
// back-compat import
|
|
409
|
+
importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
|
|
410
|
+
}
|
|
411
|
+
// Ensure there's a default export for ESM imports that expect it
|
|
412
|
+
if (!importedModule.default) {
|
|
413
|
+
importedModule = { default: importedModule, ...importedModule };
|
|
414
|
+
}
|
|
415
|
+
return importedModule;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a SourceTextModule or SyntheticModule from source code
|
|
420
|
+
*/
|
|
421
|
+
function createModuleFromSource(
|
|
422
|
+
url: string,
|
|
423
|
+
source: string,
|
|
424
|
+
usePrivateGlobal: boolean
|
|
425
|
+
): SourceTextModule | SyntheticModule {
|
|
426
|
+
// Handle JSON modules
|
|
427
|
+
if (url.endsWith('.json')) {
|
|
428
|
+
const jsonData = parseJsonModule(source, url);
|
|
429
|
+
return new SyntheticModule(
|
|
430
|
+
['default'],
|
|
300
431
|
function () {
|
|
301
|
-
|
|
302
|
-
this.setExport(key, harperExports[key]);
|
|
303
|
-
}
|
|
432
|
+
this.setExport('default', jsonData);
|
|
304
433
|
},
|
|
305
434
|
{ identifier: url, context }
|
|
306
435
|
);
|
|
307
|
-
}
|
|
308
|
-
checkAllowedModulePath(url, scope.verifyPath);
|
|
309
|
-
let source = await readFile(new URL(url), { encoding: 'utf-8' });
|
|
310
|
-
|
|
311
|
-
// Handle JSON modules as a SyntheticModule with a default export.
|
|
312
|
-
// JSON imports only support default exports per the ESM spec.
|
|
313
|
-
if (url.endsWith('.json')) {
|
|
314
|
-
const jsonData = parseJsonModule(source, url);
|
|
315
|
-
module = new SyntheticModule(
|
|
316
|
-
['default'],
|
|
317
|
-
function () {
|
|
318
|
-
this.setExport('default', jsonData);
|
|
319
|
-
},
|
|
320
|
-
{ identifier: url, context }
|
|
321
|
-
);
|
|
322
|
-
} else {
|
|
323
|
-
// Strip TypeScript types if this is a .ts file
|
|
324
|
-
if (url.endsWith('.ts') || url.endsWith('.tsx')) {
|
|
325
|
-
source = await stripTypeScriptTypes(source);
|
|
326
|
-
}
|
|
436
|
+
}
|
|
327
437
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
} catch {
|
|
332
|
-
// If CJS loading fails (likely due to ESM syntax like import/export), try ESM
|
|
333
|
-
try {
|
|
334
|
-
module = new SourceTextModule(source, {
|
|
335
|
-
identifier: url,
|
|
336
|
-
context,
|
|
337
|
-
initializeImportMeta(meta) {
|
|
338
|
-
meta.url = url;
|
|
339
|
-
},
|
|
340
|
-
async importModuleDynamically(specifier: string) {
|
|
341
|
-
const resolvedUrl = resolveModule(specifier, url);
|
|
342
|
-
const dynamicModule = await loadModuleWithCache(resolvedUrl, true);
|
|
343
|
-
return dynamicModule;
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
} catch (esmErr) {
|
|
347
|
-
// Both failed - throw the ESM error as it's likely more relevant
|
|
348
|
-
throw esmErr;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
} else {
|
|
353
|
-
const replacedModule = checkAllowedModulePath(url, scope.verifyPath);
|
|
354
|
-
// For Node.js built-in modules (node:) and npm packages
|
|
355
|
-
// Always try require first to properly handle CJS modules with named exports
|
|
356
|
-
try {
|
|
357
|
-
const cjsExports = replacedModule ?? require(url);
|
|
358
|
-
// It's a CJS module - expose all properties as named exports
|
|
359
|
-
const exportNames = Object.keys(cjsExports);
|
|
360
|
-
module = new SyntheticModule(
|
|
361
|
-
exportNames.length > 0 ? [...exportNames, 'default'] : ['default'],
|
|
362
|
-
function () {
|
|
363
|
-
if (exportNames.length > 0) {
|
|
364
|
-
for (const key of exportNames) {
|
|
365
|
-
this.setExport(key, cjsExports[key]);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
this.setExport('default', cjsExports);
|
|
369
|
-
},
|
|
370
|
-
{ identifier: url, context }
|
|
371
|
-
);
|
|
372
|
-
} catch {
|
|
373
|
-
// Fall back to dynamic import for ESM packages
|
|
374
|
-
const importedModule = await import(url);
|
|
375
|
-
const exportNames = Object.keys(importedModule);
|
|
376
|
-
module = new SyntheticModule(
|
|
377
|
-
exportNames,
|
|
378
|
-
function () {
|
|
379
|
-
for (const key of exportNames) {
|
|
380
|
-
this.setExport(key, importedModule[key]);
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
{ identifier: url, context }
|
|
384
|
-
);
|
|
385
|
-
}
|
|
438
|
+
// Strip TypeScript types if this is a .ts file
|
|
439
|
+
if (url.endsWith('.ts') || url.endsWith('.tsx')) {
|
|
440
|
+
source = stripTypeScriptTypes(source);
|
|
386
441
|
}
|
|
387
442
|
|
|
388
|
-
|
|
443
|
+
// Try CJS first since it will fail fast with clear syntax errors on ESM syntax
|
|
444
|
+
try {
|
|
445
|
+
return loadCJSModule(url, source, usePrivateGlobal);
|
|
446
|
+
} catch {
|
|
447
|
+
// If CJS loading fails (likely due to ESM syntax like import/export), try ESM
|
|
448
|
+
return new SourceTextModule(source, {
|
|
449
|
+
identifier: url,
|
|
450
|
+
context,
|
|
451
|
+
initializeImportMeta(meta) {
|
|
452
|
+
meta.url = url;
|
|
453
|
+
},
|
|
454
|
+
importModuleDynamically(specifier: string) {
|
|
455
|
+
const resolvedUrl = resolveModule(specifier, url);
|
|
456
|
+
const useContainment = specifier.startsWith('.') || scope.dependencyContainment !== false;
|
|
457
|
+
return loadModuleWithCache(resolvedUrl, useContainment);
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
389
461
|
}
|
|
390
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Create a module from URL without linking or evaluating (async version for initial load)
|
|
465
|
+
*/
|
|
466
|
+
function createModule(
|
|
467
|
+
url: string,
|
|
468
|
+
usePrivateGlobal: boolean
|
|
469
|
+
): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
|
|
470
|
+
// Handle special built-in modules
|
|
471
|
+
if (url === 'harper') {
|
|
472
|
+
return createSyntheticModule(url, getHarperExports(scope));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (url.startsWith('file://') && usePrivateGlobal) {
|
|
476
|
+
checkAllowedModulePath(url, scope.verifyPath);
|
|
477
|
+
const source = readFileSync(new URL(url), { encoding: 'utf-8' });
|
|
478
|
+
return createModuleFromSource(url, source, usePrivateGlobal);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// For Node.js built-in modules (node:) and npm packages without dependency containment
|
|
482
|
+
const replacedModule = checkAllowedModulePath(url, scope.verifyPath);
|
|
483
|
+
if (replacedModule) {
|
|
484
|
+
return createSyntheticModule(url, normalizeImportedModule(replacedModule));
|
|
485
|
+
}
|
|
486
|
+
return import(url).then((importedModule) => createSyntheticModule(url, normalizeImportedModule(importedModule)));
|
|
487
|
+
}
|
|
391
488
|
// Load the entry module
|
|
392
489
|
const entryModule = await loadModuleWithCache(moduleUrl, true);
|
|
393
490
|
|
|
@@ -433,8 +530,8 @@ async function getCompartment(scope: ApplicationScope, globals) {
|
|
|
433
530
|
},
|
|
434
531
|
};
|
|
435
532
|
} else if (moduleSpecifier.startsWith('file:') && !moduleSpecifier.includes('node_modules')) {
|
|
436
|
-
|
|
437
|
-
// Handle JSON files in
|
|
533
|
+
let moduleText = await readFile(new URL(moduleSpecifier), { encoding: 'utf-8' });
|
|
534
|
+
// Handle JSON files in compartment mode the same way as in VM mode
|
|
438
535
|
if (moduleSpecifier.endsWith('.json')) {
|
|
439
536
|
const jsonData = parseJsonModule(moduleText, moduleSpecifier);
|
|
440
537
|
return {
|
|
@@ -445,6 +542,10 @@ async function getCompartment(scope: ApplicationScope, globals) {
|
|
|
445
542
|
},
|
|
446
543
|
};
|
|
447
544
|
}
|
|
545
|
+
// Strip TypeScript types if this is a .ts file
|
|
546
|
+
if (moduleSpecifier.endsWith('.ts') || moduleSpecifier.endsWith('.tsx')) {
|
|
547
|
+
moduleText = stripTypeScriptTypes(moduleText);
|
|
548
|
+
}
|
|
448
549
|
return new StaticModuleRecord(moduleText, moduleSpecifier);
|
|
449
550
|
} else {
|
|
450
551
|
checkAllowedModulePath(moduleSpecifier, scope.verifyPath);
|
|
@@ -479,6 +580,9 @@ function secureOnlyFetch(resource, options) {
|
|
|
479
580
|
return fetch(resource, options);
|
|
480
581
|
}
|
|
481
582
|
|
|
583
|
+
// These globals need to match the literals produced in the VM context
|
|
584
|
+
const contextualizedJSGlobals = ['Object', 'Array', 'Function', 'globalThis'];
|
|
585
|
+
|
|
482
586
|
let defaultJSGlobalNames: string[];
|
|
483
587
|
// get the global variable names that are intrinsically present in a VM context (so we don't override them)
|
|
484
588
|
function getDefaultJSGlobalNames() {
|
|
@@ -494,12 +598,13 @@ function getDefaultJSGlobalNames() {
|
|
|
494
598
|
/**
|
|
495
599
|
* Get the set of global variables that should be available to modules that run in scoped compartments/contexts.
|
|
496
600
|
*/
|
|
497
|
-
function getGlobalObject(scope: ApplicationScope) {
|
|
601
|
+
function getGlobalObject(scope: ApplicationScope, copyIntrinsics = false) {
|
|
498
602
|
const appGlobal = {};
|
|
499
603
|
// create the new global object, assigning all the global variables from this global
|
|
500
604
|
// except those that will be natural intrinsics of the new VM
|
|
605
|
+
const globalsToExclude = copyIntrinsics ? contextualizedJSGlobals : getDefaultJSGlobalNames();
|
|
501
606
|
for (let name of Object.getOwnPropertyNames(global)) {
|
|
502
|
-
if (
|
|
607
|
+
if (globalsToExclude.includes(name)) continue;
|
|
503
608
|
appGlobal[name] = global[name];
|
|
504
609
|
}
|
|
505
610
|
// now assign Harper scope-specific variables
|
|
@@ -533,6 +638,25 @@ function getHarperExports(scope: ApplicationScope) {
|
|
|
533
638
|
authenticateUser: server.authenticateUser,
|
|
534
639
|
operation: server.operation,
|
|
535
640
|
contentTypes,
|
|
641
|
+
Attribute: undefined,
|
|
642
|
+
Config: undefined,
|
|
643
|
+
ConfigValue: undefined,
|
|
644
|
+
Context: undefined,
|
|
645
|
+
FileAndURLPathConfig: undefined,
|
|
646
|
+
FilesOption: undefined,
|
|
647
|
+
FilesOptionObject: undefined,
|
|
648
|
+
IterableEventQueue: undefined,
|
|
649
|
+
Logger: undefined,
|
|
650
|
+
Query: undefined,
|
|
651
|
+
RecordObject: undefined,
|
|
652
|
+
RequestTargetOrId: undefined,
|
|
653
|
+
ResourceInterface: undefined,
|
|
654
|
+
Scope: undefined,
|
|
655
|
+
Session: undefined,
|
|
656
|
+
SourceContext: undefined,
|
|
657
|
+
SubscriptionRequest: undefined,
|
|
658
|
+
Table: undefined,
|
|
659
|
+
User: undefined,
|
|
536
660
|
};
|
|
537
661
|
}
|
|
538
662
|
const ALLOWED_NODE_BUILTIN_MODULES = env.get(CONFIG_PARAMS.APPLICATIONS_ALLOWEDBUILTINMODULES)
|
|
@@ -544,14 +668,19 @@ const ALLOWED_NODE_BUILTIN_MODULES = env.get(CONFIG_PARAMS.APPLICATIONS_ALLOWEDB
|
|
|
544
668
|
},
|
|
545
669
|
};
|
|
546
670
|
const ALLOWED_COMMANDS = new Set(env.get(CONFIG_PARAMS.APPLICATIONS_ALLOWEDSPAWNCOMMANDS) ?? []);
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
671
|
+
const child_processConstrained = {
|
|
672
|
+
exec: createSpawn(child_process.exec),
|
|
673
|
+
execFile: createSpawn(child_process.execFile),
|
|
674
|
+
fork: createSpawn(child_process.fork, true), // this is launching node, so deemed safe
|
|
675
|
+
spawn: createSpawn(child_process.spawn),
|
|
676
|
+
execSync: function () {
|
|
677
|
+
throw new Error('execSync is not allowed');
|
|
553
678
|
},
|
|
554
679
|
};
|
|
680
|
+
child_processConstrained.default = child_processConstrained;
|
|
681
|
+
const REPLACED_BUILTIN_MODULES = {
|
|
682
|
+
child_process: child_processConstrained,
|
|
683
|
+
};
|
|
555
684
|
/**
|
|
556
685
|
* Creates a ChildProcess-like object for an existing process
|
|
557
686
|
*/
|
package/server/REST.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as harperLogger from '../utility/logging/harper_logger.js';
|
|
|
4
4
|
import { ServerOptions } from 'http';
|
|
5
5
|
import { ServerError, ClientError } from '../utility/errors/hdbError.js';
|
|
6
6
|
import { Resources } from '../resources/Resources.ts';
|
|
7
|
-
import { Resource } from '../resources/Resource.ts';
|
|
7
|
+
import { Resource, missingMethod, allowedMethods } from '../resources/Resource.ts';
|
|
8
8
|
import { IterableEventQueue } from '../resources/IterableEventQueue.ts';
|
|
9
9
|
import { transaction } from '../resources/transaction.ts';
|
|
10
10
|
import { Headers, mergeHeaders } from '../server/serverHelpers/Headers.ts';
|
|
@@ -114,29 +114,38 @@ async function http(request: Context & Request, nextHandler) {
|
|
|
114
114
|
switch (method) {
|
|
115
115
|
case 'GET':
|
|
116
116
|
case 'HEAD':
|
|
117
|
-
return resource.get(target, request);
|
|
117
|
+
return resource.get ? resource.get(target, request) : missingMethod(resource, 'get');
|
|
118
118
|
case 'POST':
|
|
119
|
-
return resource.post(target, request.data, request);
|
|
119
|
+
return resource.post ? resource.post(target, request.data, request) : missingMethod(resource, 'post');
|
|
120
120
|
case 'PUT':
|
|
121
|
-
return resource.put(target, request.data, request);
|
|
121
|
+
return resource.put ? resource.put(target, request.data, request) : missingMethod(resource, 'put');
|
|
122
122
|
case 'DELETE':
|
|
123
|
-
return resource.delete(target, request);
|
|
123
|
+
return resource.delete ? resource.delete(target, request) : missingMethod(resource, 'delete');
|
|
124
124
|
case 'PATCH':
|
|
125
|
-
return resource.patch(target, request.data, request);
|
|
125
|
+
return resource.patch ? resource.patch(target, request.data, request) : missingMethod(resource, 'patch');
|
|
126
126
|
case 'OPTIONS': // used primarily for CORS
|
|
127
|
-
headers.setIfNone(
|
|
127
|
+
headers.setIfNone(
|
|
128
|
+
'Allow',
|
|
129
|
+
allowedMethods(resource)
|
|
130
|
+
.map((method) => method.toUpperCase())
|
|
131
|
+
.join(', ')
|
|
132
|
+
);
|
|
128
133
|
return;
|
|
129
134
|
case 'CONNECT':
|
|
130
135
|
// websockets? and event-stream
|
|
131
|
-
return resource.connect(target, null, request);
|
|
136
|
+
return resource.connect ? resource.connect(target, null, request) : missingMethod(resource, 'connect');
|
|
132
137
|
case 'TRACE':
|
|
133
138
|
return 'Harper is the terminating server';
|
|
134
139
|
case 'QUERY':
|
|
135
|
-
return resource.query(target, request.data, request);
|
|
140
|
+
return resource.query ? resource.query(target, request.data, request) : missingMethod(resource, 'query');
|
|
136
141
|
case 'COPY': // methods suggested from webdav RFC 4918
|
|
137
|
-
return resource.copy
|
|
142
|
+
return resource.copy
|
|
143
|
+
? resource.copy(target, headersObject.destination, request)
|
|
144
|
+
: missingMethod(resource, 'copy');
|
|
138
145
|
case 'MOVE':
|
|
139
|
-
return resource.move
|
|
146
|
+
return resource.move
|
|
147
|
+
? resource.move(target, headersObject.destination, request)
|
|
148
|
+
: missingMethod(resource, 'move');
|
|
140
149
|
case 'BREW': // RFC 2324
|
|
141
150
|
throw new ClientError("Harper is short and stout and can't brew coffee", 418);
|
|
142
151
|
default:
|
|
@@ -164,8 +173,18 @@ async function http(request: Context & Request, nextHandler) {
|
|
|
164
173
|
responseData.headers = responseHeaders;
|
|
165
174
|
// if no body, look for provided data to serialize
|
|
166
175
|
if (!responseData.body) {
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
let body: any;
|
|
177
|
+
if ('data' in responseData) {
|
|
178
|
+
// a standard Response object does not have a setter for body, so we force it
|
|
179
|
+
body = serialize(responseData.data, request, responseData);
|
|
180
|
+
} else if (responseData.body === undefined) {
|
|
181
|
+
// if there is really no body, serialize this object into the body. Note that `new Response()` creates a response
|
|
182
|
+
// with a null body, and will not fall into this branch
|
|
183
|
+
body = serialize(responseData, request, responseData);
|
|
184
|
+
}
|
|
185
|
+
if (body) {
|
|
186
|
+
responseData = { status: responseData.status, headers: responseData.headers, body };
|
|
187
|
+
}
|
|
169
188
|
}
|
|
170
189
|
responseData.status ??= status ?? 200;
|
|
171
190
|
return responseData;
|
package/server/http.ts
CHANGED
|
@@ -8,7 +8,7 @@ import harperLogger from '../utility/logging/harper_logger.js';
|
|
|
8
8
|
import { parentPort } from 'node:worker_threads';
|
|
9
9
|
import env from '../utility/environment/environmentManager.js';
|
|
10
10
|
import * as terms from '../utility/hdbTerms.ts';
|
|
11
|
-
import {
|
|
11
|
+
import { getConfigPath } from '../config/configUtils.js';
|
|
12
12
|
import { getTicketKeys } from './threads/manageThreads.js';
|
|
13
13
|
import { createTLSSelector } from '../security/keys.js';
|
|
14
14
|
import { createSecureServer } from 'node:http2';
|
|
@@ -35,6 +35,7 @@ const httpServers = {},
|
|
|
35
35
|
httpChain = {},
|
|
36
36
|
httpResponders = [];
|
|
37
37
|
let httpOptions: HttpOptions = {};
|
|
38
|
+
export const universalHeaders: [string, string][] = [];
|
|
38
39
|
|
|
39
40
|
export function handleApplication(scope: Scope) {
|
|
40
41
|
httpOptions = scope.options.getAll() as HttpOptions;
|
|
@@ -181,7 +182,7 @@ function getPorts(options) {
|
|
|
181
182
|
|
|
182
183
|
if (options?.usageType === 'operations-api' && env.get(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET)) {
|
|
183
184
|
ports.push({
|
|
184
|
-
port:
|
|
185
|
+
port: getConfigPath(terms.CONFIG_PARAMS.OPERATIONSAPI_NETWORK_DOMAINSOCKET),
|
|
185
186
|
secure: false,
|
|
186
187
|
});
|
|
187
188
|
}
|
|
@@ -269,7 +270,9 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
|
|
|
269
270
|
if (!response.headers?.set) {
|
|
270
271
|
response.headers = new Headers(response.headers);
|
|
271
272
|
}
|
|
272
|
-
|
|
273
|
+
for (let [key, value] of universalHeaders) {
|
|
274
|
+
response.headers.set(key, value);
|
|
275
|
+
}
|
|
273
276
|
if (response.status === -1) {
|
|
274
277
|
// This means the HDB stack didn't handle the request, and we can then cascade the request
|
|
275
278
|
// to the server-level handler, forming the bridge to the slower legacy fastify framework that expects
|
|
@@ -149,7 +149,7 @@ async function componentStatusRequestHandler(event) {
|
|
|
149
149
|
if (originatorThreadId === undefined) {
|
|
150
150
|
hdbLogger.debug('No originator threadId, falling back to broadcast');
|
|
151
151
|
} else {
|
|
152
|
-
hdbLogger.
|
|
152
|
+
hdbLogger.debug(`Failed to send direct response to thread ${originatorThreadId}, falling back to broadcast`);
|
|
153
153
|
}
|
|
154
154
|
await sendItcEvent(responseMessage);
|
|
155
155
|
}
|