@harperfast/harper 5.0.0-beta.7 → 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/components/ApplicationScope.ts +1 -0
- package/components/componentLoader.ts +4 -1
- package/dist/bin/cliOperations.js +3 -3
- package/dist/bin/cliOperations.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 +2 -1
- package/dist/components/componentLoader.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/Table.d.ts +6 -6
- package/dist/resources/Table.js +15 -10
- package/dist/resources/Table.js.map +1 -1
- package/dist/security/jsLoader.js +123 -90
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +13 -4
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +5 -0
- package/dist/server/http.js.map +1 -1
- package/package.json +3 -3
- package/resources/DatabaseTransaction.ts +67 -32
- package/resources/Table.ts +29 -10
- package/security/jsLoader.ts +149 -101
- package/server/REST.ts +12 -2
- package/server/http.ts +4 -1
- package/studio/web/assets/{index-ClD_q6ya.js → index-CXQsBaYq.js} +5 -5
- package/studio/web/assets/{index-ClD_q6ya.js.map → index-CXQsBaYq.js.map} +1 -1
- package/studio/web/assets/{index.lazy-CXzU1gVu.js → index.lazy-C3Ejfvna.js} +2 -2
- package/studio/web/assets/{index.lazy-CXzU1gVu.js.map → index.lazy-C3Ejfvna.js.map} +1 -1
- package/studio/web/assets/{profile-DCNVg5yY.js → profile-BbbbWJCN.js} +2 -2
- package/studio/web/assets/{profile-DCNVg5yY.js.map → profile-BbbbWJCN.js.map} +1 -1
- package/studio/web/assets/{status-CoGlcjSB.js → status-CFe85l8C.js} +2 -2
- package/studio/web/assets/{status-CoGlcjSB.js.map → status-CFe85l8C.js.map} +1 -1
- package/studio/web/index.html +1 -1
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,13 +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
|
-
|
|
155
|
-
|
|
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;
|
|
156
174
|
|
|
157
175
|
/**
|
|
158
176
|
* Resolve module specifier to absolute URL
|
|
@@ -298,9 +316,11 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
298
316
|
}
|
|
299
317
|
|
|
300
318
|
/**
|
|
301
|
-
* Linker function for module resolution during instantiation
|
|
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.
|
|
302
322
|
*/
|
|
303
|
-
|
|
323
|
+
function linker(specifier: string, referencingModule: SourceTextModule | SyntheticModule) {
|
|
304
324
|
const resolvedUrl = resolveModule(specifier, referencingModule.identifier);
|
|
305
325
|
|
|
306
326
|
// Determine if we should use VM containment for this module
|
|
@@ -316,15 +336,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
316
336
|
}
|
|
317
337
|
}
|
|
318
338
|
|
|
319
|
-
// Return the module
|
|
320
|
-
return
|
|
339
|
+
// Return the module
|
|
340
|
+
return getOrCreateModule(resolvedUrl, useContainment);
|
|
321
341
|
}
|
|
322
342
|
|
|
323
|
-
|
|
343
|
+
function getOrCreateModule(
|
|
324
344
|
url: string,
|
|
325
345
|
usePrivateGlobal: boolean
|
|
326
|
-
): Promise<SourceTextModule | SyntheticModule> {
|
|
327
|
-
// Check
|
|
346
|
+
): SourceTextModule | SyntheticModule | Promise<SourceTextModule | SyntheticModule> {
|
|
347
|
+
// Check if module is already created
|
|
328
348
|
if (moduleCache.has(url)) {
|
|
329
349
|
return moduleCache.get(url)!;
|
|
330
350
|
}
|
|
@@ -345,8 +365,15 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
345
365
|
// Only link/evaluate once per module
|
|
346
366
|
if (!linkingPromises.has(url)) {
|
|
347
367
|
const linkingPromise = (async () => {
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
|
350
377
|
})();
|
|
351
378
|
linkingPromises.set(url, linkingPromise);
|
|
352
379
|
}
|
|
@@ -357,94 +384,107 @@ async function loadModuleWithVM(moduleUrl: string, scope: ApplicationScope) {
|
|
|
357
384
|
return module;
|
|
358
385
|
}
|
|
359
386
|
/**
|
|
360
|
-
* Create a
|
|
387
|
+
* Create a SyntheticModule from exported object
|
|
361
388
|
*/
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
Object.keys(harperExports),
|
|
370
|
-
function () {
|
|
371
|
-
for (let key in harperExports) {
|
|
372
|
-
this.setExport(key, harperExports[key]);
|
|
373
|
-
}
|
|
374
|
-
},
|
|
375
|
-
{ identifier: url, context }
|
|
376
|
-
);
|
|
377
|
-
} else if (url.startsWith('file://') && usePrivateGlobal) {
|
|
378
|
-
checkAllowedModulePath(url, scope.verifyPath);
|
|
379
|
-
let source = await readFile(new URL(url), { encoding: 'utf-8' });
|
|
380
|
-
|
|
381
|
-
// Handle JSON modules as a SyntheticModule with a default export.
|
|
382
|
-
// JSON imports only support default exports per the ESM spec.
|
|
383
|
-
if (url.endsWith('.json')) {
|
|
384
|
-
const jsonData = parseJsonModule(source, url);
|
|
385
|
-
module = new SyntheticModule(
|
|
386
|
-
['default'],
|
|
387
|
-
function () {
|
|
388
|
-
this.setExport('default', jsonData);
|
|
389
|
-
},
|
|
390
|
-
{ identifier: url, context }
|
|
391
|
-
);
|
|
392
|
-
} else {
|
|
393
|
-
// Strip TypeScript types if this is a .ts file
|
|
394
|
-
if (url.endsWith('.ts') || url.endsWith('.tsx')) {
|
|
395
|
-
source = await stripTypeScriptTypes(source);
|
|
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
396
|
}
|
|
397
|
+
},
|
|
398
|
+
{ identifier: url, context }
|
|
399
|
+
);
|
|
400
|
+
}
|
|
397
401
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
let importedModule = replacedModule ?? (await import(url));
|
|
428
|
-
const cjsModule = importedModule['module.exports'];
|
|
429
|
-
if (cjsModule) {
|
|
430
|
-
// back-compat import
|
|
431
|
-
importedModule = importedModule.default ? { default: importedModule.default, ...cjsModule } : cjsModule;
|
|
432
|
-
}
|
|
433
|
-
const exportNames = Object.keys(importedModule);
|
|
434
|
-
module = new SyntheticModule(
|
|
435
|
-
exportNames,
|
|
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'],
|
|
436
431
|
function () {
|
|
437
|
-
|
|
438
|
-
this.setExport(key, importedModule[key]);
|
|
439
|
-
}
|
|
432
|
+
this.setExport('default', jsonData);
|
|
440
433
|
},
|
|
441
434
|
{ identifier: url, context }
|
|
442
435
|
);
|
|
443
436
|
}
|
|
444
437
|
|
|
445
|
-
|
|
438
|
+
// Strip TypeScript types if this is a .ts file
|
|
439
|
+
if (url.endsWith('.ts') || url.endsWith('.tsx')) {
|
|
440
|
+
source = stripTypeScriptTypes(source);
|
|
441
|
+
}
|
|
442
|
+
|
|
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
|
+
}
|
|
446
461
|
}
|
|
447
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
|
+
}
|
|
448
488
|
// Load the entry module
|
|
449
489
|
const entryModule = await loadModuleWithCache(moduleUrl, true);
|
|
450
490
|
|
|
@@ -490,8 +530,8 @@ async function getCompartment(scope: ApplicationScope, globals) {
|
|
|
490
530
|
},
|
|
491
531
|
};
|
|
492
532
|
} else if (moduleSpecifier.startsWith('file:') && !moduleSpecifier.includes('node_modules')) {
|
|
493
|
-
|
|
494
|
-
// 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
|
|
495
535
|
if (moduleSpecifier.endsWith('.json')) {
|
|
496
536
|
const jsonData = parseJsonModule(moduleText, moduleSpecifier);
|
|
497
537
|
return {
|
|
@@ -502,6 +542,10 @@ async function getCompartment(scope: ApplicationScope, globals) {
|
|
|
502
542
|
},
|
|
503
543
|
};
|
|
504
544
|
}
|
|
545
|
+
// Strip TypeScript types if this is a .ts file
|
|
546
|
+
if (moduleSpecifier.endsWith('.ts') || moduleSpecifier.endsWith('.tsx')) {
|
|
547
|
+
moduleText = stripTypeScriptTypes(moduleText);
|
|
548
|
+
}
|
|
505
549
|
return new StaticModuleRecord(moduleText, moduleSpecifier);
|
|
506
550
|
} else {
|
|
507
551
|
checkAllowedModulePath(moduleSpecifier, scope.verifyPath);
|
|
@@ -536,6 +580,9 @@ function secureOnlyFetch(resource, options) {
|
|
|
536
580
|
return fetch(resource, options);
|
|
537
581
|
}
|
|
538
582
|
|
|
583
|
+
// These globals need to match the literals produced in the VM context
|
|
584
|
+
const contextualizedJSGlobals = ['Object', 'Array', 'Function', 'globalThis'];
|
|
585
|
+
|
|
539
586
|
let defaultJSGlobalNames: string[];
|
|
540
587
|
// get the global variable names that are intrinsically present in a VM context (so we don't override them)
|
|
541
588
|
function getDefaultJSGlobalNames() {
|
|
@@ -551,12 +598,13 @@ function getDefaultJSGlobalNames() {
|
|
|
551
598
|
/**
|
|
552
599
|
* Get the set of global variables that should be available to modules that run in scoped compartments/contexts.
|
|
553
600
|
*/
|
|
554
|
-
function getGlobalObject(scope: ApplicationScope) {
|
|
601
|
+
function getGlobalObject(scope: ApplicationScope, copyIntrinsics = false) {
|
|
555
602
|
const appGlobal = {};
|
|
556
603
|
// create the new global object, assigning all the global variables from this global
|
|
557
604
|
// except those that will be natural intrinsics of the new VM
|
|
605
|
+
const globalsToExclude = copyIntrinsics ? contextualizedJSGlobals : getDefaultJSGlobalNames();
|
|
558
606
|
for (let name of Object.getOwnPropertyNames(global)) {
|
|
559
|
-
if (
|
|
607
|
+
if (globalsToExclude.includes(name)) continue;
|
|
560
608
|
appGlobal[name] = global[name];
|
|
561
609
|
}
|
|
562
610
|
// now assign Harper scope-specific variables
|
package/server/REST.ts
CHANGED
|
@@ -173,8 +173,18 @@ async function http(request: Context & Request, nextHandler) {
|
|
|
173
173
|
responseData.headers = responseHeaders;
|
|
174
174
|
// if no body, look for provided data to serialize
|
|
175
175
|
if (!responseData.body) {
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|
|
178
188
|
}
|
|
179
189
|
responseData.status ??= status ?? 200;
|
|
180
190
|
return responseData;
|
package/server/http.ts
CHANGED
|
@@ -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;
|
|
@@ -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
|