feat/ephemeral-keys #38
15 changed files with 305 additions and 2537 deletions
|
|
@ -38,7 +38,7 @@ client.dispose();
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.js';
|
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.mjs';
|
||||||
|
|
||||||
const client = new SecureChatCompletion({
|
const client = new SecureChatCompletion({
|
||||||
baseUrl: 'https://api.nomyo.ai',
|
baseUrl: 'https://api.nomyo.ai',
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ console.log('Answer:', content); // final answer to the user
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.js';
|
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.mjs';
|
||||||
|
|
||||||
// In production, proxy through your backend instead of exposing the API key
|
// In production, proxy through your backend instead of exposing the API key
|
||||||
const client = new SecureChatCompletion({
|
const client = new SecureChatCompletion({
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ In browsers, keys are kept in memory only (no file system). Everything else is i
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.js';
|
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.mjs';
|
||||||
|
|
||||||
const client = new SecureChatCompletion({
|
const client = new SecureChatCompletion({
|
||||||
baseUrl: 'https://api.nomyo.ai',
|
baseUrl: 'https://api.nomyo.ai',
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pnpm add nomyo-js
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.js';
|
import { SecureChatCompletion } from 'https://unpkg.com/nomyo-js/dist/browser/index.mjs';
|
||||||
// ...
|
// ...
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/tests/unit/**/*.test.ts', '**/tests/integration/**/*.test.ts'],
|
testMatch: ['**/tests/unit/**/*.test.ts', '**/tests/integration/**/*.test.ts'],
|
||||||
|
setupFiles: ['<rootDir>/tests/setup.ts'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx?$': ['ts-jest', {
|
'^.+\\.tsx?$': ['ts-jest', {
|
||||||
tsconfig: {
|
tsconfig: {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"gypfile": true,
|
"gypfile": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"node-addon-api": "^8.6.0",
|
||||||
"node-gyp-build": "^4.8.0"
|
"node-gyp-build": "^4.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
2495
package-lock.json
generated
2495
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
|
@ -3,35 +3,39 @@
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"description": "OpenAI-compatible secure chat client with end-to-end encryption",
|
"description": "OpenAI-compatible secure chat client with end-to-end encryption",
|
||||||
"main": "dist/node/index.js",
|
"main": "dist/node/index.js",
|
||||||
"browser": "dist/browser/index.js",
|
"browser": "dist/browser/index.mjs",
|
||||||
"module": "dist/esm/index.js",
|
"module": "dist/esm/index.mjs",
|
||||||
"types": "dist/types/index.d.ts",
|
"types": "dist/types/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"node": {
|
"types": "./dist/types/index.d.ts",
|
||||||
"require": "./dist/node/index.js",
|
|
||||||
"import": "./dist/esm/index.js"
|
|
||||||
},
|
|
||||||
"browser": {
|
"browser": {
|
||||||
"import": "./dist/browser/index.js",
|
"import": "./dist/browser/index.mjs",
|
||||||
"require": "./dist/browser/index.js"
|
"require": "./dist/browser/index.cjs"
|
||||||
},
|
},
|
||||||
"types": "./dist/types/index.d.ts"
|
"node": {
|
||||||
|
"import": "./dist/esm/index.mjs",
|
||||||
|
"require": "./dist/node/index.js"
|
||||||
|
},
|
||||||
|
"default": "./dist/browser/index.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"native",
|
"native/binding.gyp",
|
||||||
|
"native/index.js",
|
||||||
|
"native/package.json",
|
||||||
|
"native/src",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:node && npm run build:browser && npm run build:types",
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
||||||
|
"build": "npm run clean && npm run build:node && npm run build:browser && npm run build:types",
|
||||||
"build:node": "rollup -c --environment TARGET:node",
|
"build:node": "rollup -c --environment TARGET:node",
|
||||||
"build:browser": "rollup -c --environment TARGET:browser",
|
"build:browser": "rollup -c --environment TARGET:browser",
|
||||||
"build:types": "tsc --emitDeclarationOnly",
|
"build:types": "tsc --emitDeclarationOnly",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:browser": "karma start",
|
|
||||||
"prepublishOnly": "npm run build && npm test"
|
"prepublishOnly": "npm run build && npm test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
@ -55,7 +59,6 @@
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"karma": "^6.4.0",
|
|
||||||
"node-addon-api": "^8.6.0",
|
"node-addon-api": "^8.6.0",
|
||||||
"node-gyp": "^13.0.0",
|
"node-gyp": "^13.0.0",
|
||||||
"node-gyp-build": "^4.8.0",
|
"node-gyp-build": "^4.8.0",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ const config = {
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extensions are explicit rather than bare `.js`: the package is not
|
||||||
|
// "type": "module", so Node treats a .js file as CommonJS. An ES-format bundle
|
||||||
|
// named .js therefore fails to import on Node < 22, and only works above that
|
||||||
|
// because Node re-parses it after guessing — at a cost on every import.
|
||||||
if (target === 'node') {
|
if (target === 'node') {
|
||||||
config.output = [
|
config.output = [
|
||||||
{
|
{
|
||||||
|
|
@ -25,17 +29,33 @@ if (target === 'node') {
|
||||||
exports: 'named'
|
exports: 'named'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
file: 'dist/esm/index.js',
|
file: 'dist/esm/index.mjs',
|
||||||
format: 'es'
|
format: 'es',
|
||||||
|
// The source loads several modules lazily via require(): the crypto
|
||||||
|
// fallback, fs/path for key persistence, the optional native addon,
|
||||||
|
// and jose. `require` does not exist in ES module scope, so without
|
||||||
|
// this shim an ESM consumer crashes with "require is not defined"
|
||||||
|
// the moment one of those paths runs. Node-only output, so
|
||||||
|
// node:module is safe here.
|
||||||
|
banner: "import { createRequire as __nomyoCreateRequire } from 'node:module';\n"
|
||||||
|
+ 'const require = __nomyoCreateRequire(import.meta.url);'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
config.external = ['crypto', 'https', 'fs', 'path'];
|
config.external = ['crypto', 'https', 'fs', 'path'];
|
||||||
} else if (target === 'browser') {
|
} else if (target === 'browser') {
|
||||||
config.output = {
|
config.output = [
|
||||||
file: 'dist/browser/index.js',
|
{
|
||||||
format: 'es',
|
file: 'dist/browser/index.mjs',
|
||||||
name: 'Nomyo'
|
format: 'es',
|
||||||
};
|
name: 'Nomyo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'dist/browser/index.cjs',
|
||||||
|
format: 'cjs',
|
||||||
|
exports: 'named',
|
||||||
|
name: 'Nomyo'
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* Browser-specific entry point
|
* Browser-specific entry point.
|
||||||
* Ensures browser-specific implementations are used
|
*
|
||||||
|
* Registers the browser platform implementations before anything can ask for
|
||||||
|
* them, so the bundled build never needs a runtime path lookup. Wiring them in
|
||||||
|
* here also keeps the Node.js implementations — and their `https`/`fs` imports
|
||||||
|
* and the optional native addon — out of the browser bundle entirely.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { registerSecureMemory } from './core/memory/secure';
|
||||||
|
import { registerHttpClient } from './core/http/client';
|
||||||
|
import { BrowserSecureMemory } from './core/memory/browser';
|
||||||
|
import { BrowserHttpClient } from './core/http/browser';
|
||||||
|
|
||||||
|
registerSecureMemory(() => new BrowserSecureMemory());
|
||||||
|
registerHttpClient(() => new BrowserHttpClient());
|
||||||
|
|
||||||
export * from './index';
|
export * from './index';
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,30 @@ export interface HttpClient {
|
||||||
get(url: string, options?: Omit<HttpRequestOptions, 'body'>): Promise<HttpResponse>;
|
get(url: string, options?: Omit<HttpRequestOptions, 'body'>): Promise<HttpResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HttpClientFactory = () => HttpClient;
|
||||||
|
|
||||||
|
let httpClientFactory: HttpClientFactory | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an HTTP client for the current platform
|
* Register the platform's HttpClient implementation.
|
||||||
|
*
|
||||||
|
* Called by the entry points (src/node.ts, src/browser.ts) at load time — see
|
||||||
|
* registerSecureMemory for why a runtime require cannot survive bundling.
|
||||||
|
*/
|
||||||
|
export function registerHttpClient(factory: HttpClientFactory): void {
|
||||||
|
httpClientFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HTTP client for the current platform.
|
||||||
*/
|
*/
|
||||||
export function createHttpClient(): HttpClient {
|
export function createHttpClient(): HttpClient {
|
||||||
if (typeof window !== 'undefined') {
|
if (httpClientFactory === null) {
|
||||||
// Browser environment
|
throw new Error(
|
||||||
const BrowserHttpClient = require('./browser').BrowserHttpClient;
|
'No HttpClient implementation registered. Import the package entry ' +
|
||||||
return new BrowserHttpClient();
|
"point ('nomyo-js', or src/node.ts / src/browser.ts) rather than " +
|
||||||
} else {
|
'deep-importing core modules.'
|
||||||
// Node.js environment
|
);
|
||||||
const NodeHttpClient = require('./node').NodeHttpClient;
|
|
||||||
return new NodeHttpClient();
|
|
||||||
}
|
}
|
||||||
|
return httpClientFactory();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,17 +144,32 @@ export class SecureByteContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SecureMemoryFactory = () => SecureMemory;
|
||||||
|
|
||||||
|
let secureMemoryFactory: SecureMemoryFactory | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a secure memory implementation for the current platform
|
* Register the platform's SecureMemory implementation.
|
||||||
|
*
|
||||||
|
* The entry points (src/node.ts, src/browser.ts) call this at load time. That
|
||||||
|
* matters for the bundled builds: rollup flattens every module into one file,
|
||||||
|
* so requiring the platform module by relative path at runtime would point at a
|
||||||
|
* path that no longer exists, and throw the moment platform code was needed.
|
||||||
|
*/
|
||||||
|
export function registerSecureMemory(factory: SecureMemoryFactory): void {
|
||||||
|
secureMemoryFactory = factory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a secure memory implementation for the current platform.
|
||||||
*/
|
*/
|
||||||
export function createSecureMemory(): SecureMemory {
|
export function createSecureMemory(): SecureMemory {
|
||||||
if (typeof window !== 'undefined') {
|
if (secureMemoryFactory === null) {
|
||||||
// Browser environment
|
throw new Error(
|
||||||
const BrowserSecureMemory = require('./browser').BrowserSecureMemory;
|
'No SecureMemory implementation registered. Import the package entry ' +
|
||||||
return new BrowserSecureMemory();
|
"point ('nomyo-js', or src/node.ts / src/browser.ts) rather than " +
|
||||||
} else {
|
'deep-importing core modules.'
|
||||||
// Node.js environment
|
);
|
||||||
const NodeSecureMemory = require('./node').NodeSecureMemory;
|
|
||||||
return new NodeSecureMemory();
|
|
||||||
}
|
}
|
||||||
|
return secureMemoryFactory();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/node.ts
16
src/node.ts
|
|
@ -1,6 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* Node.js-specific entry point
|
* Node.js-specific entry point.
|
||||||
* Ensures Node.js-specific implementations are used
|
*
|
||||||
|
* Registers the Node.js platform implementations before anything can ask for
|
||||||
|
* them. This is what makes the bundled build work: rollup flattens all modules
|
||||||
|
* into a single file, so the platform layer cannot be discovered at runtime by
|
||||||
|
* relative path — it has to be wired in here, statically.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { registerSecureMemory } from './core/memory/secure';
|
||||||
|
import { registerHttpClient } from './core/http/client';
|
||||||
|
import { NodeSecureMemory } from './core/memory/node';
|
||||||
|
import { NodeHttpClient } from './core/http/node';
|
||||||
|
|
||||||
|
registerSecureMemory(() => new NodeSecureMemory());
|
||||||
|
registerHttpClient(() => new NodeHttpClient());
|
||||||
|
|
||||||
export * from './index';
|
export * from './index';
|
||||||
|
|
|
||||||
164
tests/integration/bundle.test.ts
Normal file
164
tests/integration/bundle.test.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
/**
|
||||||
|
* Smoke tests for the BUILT bundles in dist/.
|
||||||
|
*
|
||||||
|
* Every other suite runs against src/ through ts-jest, where the platform
|
||||||
|
* modules resolve as ordinary relative paths. That masked a bug in which the
|
||||||
|
* published package loaded fine but threw "Cannot find module './node'" as soon
|
||||||
|
* as anything touched the platform layer — rollup flattens the modules, so the
|
||||||
|
* runtime require had nothing to resolve. The client could not be constructed
|
||||||
|
* at all from the npm package.
|
||||||
|
*
|
||||||
|
* These tests exercise the artefact that actually ships. Run `npm run build`
|
||||||
|
* first; they skip themselves if dist/ is absent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
|
const DIST = path.join(__dirname, '..', '..', 'dist');
|
||||||
|
const NODE_CJS = path.join(DIST, 'node', 'index.js');
|
||||||
|
const BROWSER_CJS = path.join(DIST, 'browser', 'index.cjs');
|
||||||
|
const ESM_ENTRY = path.join(DIST, 'esm', 'index.mjs');
|
||||||
|
|
||||||
|
const built = fs.existsSync(NODE_CJS);
|
||||||
|
const describeIfBuilt = built ? describe : describe.skip;
|
||||||
|
|
||||||
|
if (!built) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('dist/ not found — run `npm run build` to exercise the bundle smoke tests');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NomyoModule {
|
||||||
|
SecureChatCompletion: new (config?: object) => { dispose(): void };
|
||||||
|
SecureCompletionClient: new (config: object) => { dispose(): void };
|
||||||
|
getMemoryProtectionInfo: () => { method: string; canLock: boolean };
|
||||||
|
AttestationPolicy: new (options?: object) => { enforce: boolean };
|
||||||
|
SecurityError: new (message: string) => Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
describeIfBuilt('built Node bundle', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const nomyo = require(NODE_CJS) as NomyoModule;
|
||||||
|
|
||||||
|
test('exposes the public API', () => {
|
||||||
|
expect(typeof nomyo.SecureChatCompletion).toBe('function');
|
||||||
|
expect(typeof nomyo.SecureCompletionClient).toBe('function');
|
||||||
|
expect(typeof nomyo.getMemoryProtectionInfo).toBe('function');
|
||||||
|
expect(typeof nomyo.AttestationPolicy).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructs a client — the platform layer resolves', () => {
|
||||||
|
// This is the exact call that threw "Cannot find module './node'"
|
||||||
|
const client = new nomyo.SecureChatCompletion({});
|
||||||
|
expect(client).toBeTruthy();
|
||||||
|
client.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructs the low-level client too', () => {
|
||||||
|
const client = new nomyo.SecureCompletionClient({
|
||||||
|
routerUrl: 'https://router.test',
|
||||||
|
keyRotationInterval: 0,
|
||||||
|
});
|
||||||
|
expect(client).toBeTruthy();
|
||||||
|
client.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reaches the real secure-memory implementation', () => {
|
||||||
|
const info = nomyo.getMemoryProtectionInfo();
|
||||||
|
expect(['mlock', 'zero-only', 'none']).toContain(info.method);
|
||||||
|
expect(typeof info.canLock).toBe('boolean');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('carries no unresolved runtime requires', () => {
|
||||||
|
const source = fs.readFileSync(NODE_CJS, 'utf-8');
|
||||||
|
// './node' / './browser' only ever resolved pre-bundling
|
||||||
|
expect(source).not.toMatch(/require\(['"]\.\/node['"]\)/);
|
||||||
|
expect(source).not.toMatch(/require\(['"]\.\/browser['"]\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('attestation policy survives bundling', () => {
|
||||||
|
const policy = new nomyo.AttestationPolicy({ enforce: true });
|
||||||
|
expect(policy.enforce).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describeIfBuilt('built browser bundle', () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const nomyo = require(BROWSER_CJS) as NomyoModule;
|
||||||
|
|
||||||
|
test('exposes the public API', () => {
|
||||||
|
expect(typeof nomyo.SecureChatCompletion).toBe('function');
|
||||||
|
expect(typeof nomyo.getMemoryProtectionInfo).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('constructs a client without a DOM', () => {
|
||||||
|
const client = new nomyo.SecureChatCompletion({});
|
||||||
|
expect(client).toBeTruthy();
|
||||||
|
client.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reports browser memory protection', () => {
|
||||||
|
const info = nomyo.getMemoryProtectionInfo();
|
||||||
|
expect(info.canLock).toBe(false);
|
||||||
|
expect(info.method).toBe('zero-only');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('carries no unresolved runtime requires', () => {
|
||||||
|
const source = fs.readFileSync(BROWSER_CJS, 'utf-8');
|
||||||
|
expect(source).not.toMatch(/require\(['"]\.\/node['"]\)/);
|
||||||
|
expect(source).not.toMatch(/require\(['"]\.\/browser['"]\)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not drag the Node platform layer into the browser build', () => {
|
||||||
|
const source = fs.readFileSync(BROWSER_CJS, 'utf-8');
|
||||||
|
// The Node HTTP client and the optional native addon must not appear.
|
||||||
|
// `fs`/`path` legitimately do: KeyManager has Node-only branches guarded
|
||||||
|
// by `typeof window`, shared by both builds.
|
||||||
|
for (const nodeOnly of ['https', 'http', 'url', 'nomyo-native', 'node-gyp-build']) {
|
||||||
|
expect(source).not.toMatch(
|
||||||
|
new RegExp(`require\\(['"]${nodeOnly}['"]\\)`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describeIfBuilt('built ESM entry point', () => {
|
||||||
|
test('is real ESM with an unambiguous extension', () => {
|
||||||
|
// A .js ES module in a non-"type":"module" package is parsed as
|
||||||
|
// CommonJS by Node and fails on older versions
|
||||||
|
expect(fs.existsSync(ESM_ENTRY)).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(DIST, 'esm', 'index.js'))).toBe(false);
|
||||||
|
expect(fs.readFileSync(ESM_ENTRY, 'utf-8')).toMatch(/^export |[\n;]export /);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shims require() for its lazily-loaded modules', () => {
|
||||||
|
// The source loads crypto/fs/path/jose/the native addon via require().
|
||||||
|
// `require` is not defined in ES module scope, so the bundle must
|
||||||
|
// provide it or those paths throw ReferenceError at runtime.
|
||||||
|
const source = fs.readFileSync(ESM_ENTRY, 'utf-8');
|
||||||
|
if (/[^.\w]require\(/.test(source)) {
|
||||||
|
expect(source).toMatch(/createRequire/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('is importable and usable by a real Node process', () => {
|
||||||
|
// Not `await import(...)` — ts-jest compiles dynamic import down to
|
||||||
|
// require() for its CommonJS target, which cannot load .mjs and would
|
||||||
|
// test the compiler rather than the bundle. Spawn Node instead.
|
||||||
|
const script = `import(${JSON.stringify(ESM_ENTRY)})`
|
||||||
|
+ '.then(m => {'
|
||||||
|
+ ' const c = new m.SecureChatCompletion({}); c.dispose();'
|
||||||
|
// Touches the require()-dependent crypto path
|
||||||
|
+ ' if (typeof m.getMemoryProtectionInfo().method !== "string") process.exit(2);'
|
||||||
|
+ '})'
|
||||||
|
+ '.catch(e => { console.error(e.message); process.exit(3); })';
|
||||||
|
|
||||||
|
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.stderr.trim()).toBe('');
|
||||||
|
expect(result.status).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
14
tests/setup.ts
Normal file
14
tests/setup.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Jest setup: register the Node.js platform implementations.
|
||||||
|
*
|
||||||
|
* The unit suites import core modules directly rather than going through the
|
||||||
|
* package entry point, so nothing would otherwise call registerSecureMemory /
|
||||||
|
* registerHttpClient. Importing src/node.ts performs that registration exactly
|
||||||
|
* as the published Node bundle does.
|
||||||
|
*
|
||||||
|
* Doing this here rather than falling back to a runtime `require('./node')`
|
||||||
|
* inside the core modules keeps that require out of the built bundles, where
|
||||||
|
* the relative path does not exist and bundlers would fail to resolve it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../src/node';
|
||||||
Loading…
Add table
Add a link
Reference in a new issue