Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
"wordprocessingml",
"spreadsheetml",
"presentationml",
"unuse"
"unuse",
"workerd"
],
"ignoreRegExpList": [
"/from\\s+(['\"]).*\\1/",
Expand Down
14 changes: 14 additions & 0 deletions packages/core/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,20 @@ NativeModules.getModule('node:crypto'); // the module, or undefined if init() wa
NativeModules.names(); // ['node:crypto']
```

## Runtime

```typescript
import { Runtime, RuntimeEngine, RuntimeOs } from '@3sixty/core';

Runtime.engine(); // 'node', 'bun', 'deno', 'workerd', 'browser', 'webWorker' or 'unknown'
Runtime.version(); // e.g. '24.18.0' on Node, the user agent in a browser
Runtime.os(); // 'windows', 'mac', 'linux' or 'unknown'

if (Runtime.engine() === RuntimeEngine.Bun && Runtime.os() === RuntimeOs.Linux) {
// Bun specific handling
}
```

## RandomHelper and NumberHelper

```typescript
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export * from "./models/ISharedObjectBufferWorkerMessage.js";
export * from "./models/IUrlParts.js";
export * from "./models/IValidationFailure.js";
export * from "./models/mutexMessageTypes.js";
export * from "./models/runtimeEngine.js";
export * from "./models/runtimeOs.js";
export * from "./models/sharedObjectBufferMessageTypes.js";
export * from "./types/bitString.js";
export * from "./types/duration.js";
Expand All @@ -70,6 +72,7 @@ export * from "./utils/i18n.js";
export * from "./utils/is.js";
export * from "./utils/mutex.js";
export * from "./utils/nativeModules.js";
export * from "./utils/runtime.js";
export * from "./utils/sharedObjectBuffer.js";
export * from "./utils/sharedStore.js";
export * from "./utils/validation.js";
48 changes: 48 additions & 0 deletions packages/core/src/models/runtimeEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2026 3Sixty Trade Ltd.
// SPDX-License-Identifier: Apache-2.0.

/**
* The JavaScript runtime engines that can be detected.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const RuntimeEngine = {
/**
* Node.js.
*/
Node: "node",

/**
* Bun.
*/
Bun: "bun",

/**
* Deno.
*/
Deno: "deno",

/**
* Cloudflare Workers.
*/
Workerd: "workerd",

/**
* A browser window.
*/
Browser: "browser",

/**
* A web worker.
*/
WebWorker: "webWorker",

/**
* A runtime that could not be identified.
*/
Unknown: "unknown"
} as const;

/**
* The JavaScript runtime engines that can be detected.
*/
export type RuntimeEngine = (typeof RuntimeEngine)[keyof typeof RuntimeEngine];
33 changes: 33 additions & 0 deletions packages/core/src/models/runtimeOs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2026 3Sixty Trade Ltd.
// SPDX-License-Identifier: Apache-2.0.

/**
* The operating systems that can be detected.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export const RuntimeOs = {
/**
* Windows.
*/
Windows: "windows",

/**
* macOS.
*/
Mac: "mac",

/**
* Linux.
*/
Linux: "linux",

/**
* An operating system that could not be identified.
*/
Unknown: "unknown"
} as const;

/**
* The operating systems that can be detected.
*/
export type RuntimeOs = (typeof RuntimeOs)[keyof typeof RuntimeOs];
113 changes: 113 additions & 0 deletions packages/core/src/utils/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2026 3Sixty Trade Ltd.
// SPDX-License-Identifier: Apache-2.0.
import { Is } from "./is.js";
import { NativeModules } from "./nativeModules.js";
import { RuntimeEngine } from "../models/runtimeEngine.js";
import { RuntimeOs } from "../models/runtimeOs.js";

/**
* Detect the JavaScript runtime the code is executing in.
*/
export class Runtime {
/**
* Runtime name for the class.
*/
public static readonly CLASS_NAME: string = nameof<Runtime>();

/**
* Get the runtime engine. Bun, Deno and Cloudflare Workers are checked before Node
* as they all provide a Node compatible process global.
* @returns The runtime engine.
*/
public static engine(): RuntimeEngine {
if (Is.stringValue(Runtime.processVersions()?.bun)) {
return RuntimeEngine.Bun;
}
if (Is.object(NativeModules.getType("Deno"))) {
return RuntimeEngine.Deno;
}
if (Runtime.userAgent() === "Cloudflare-Workers") {
return RuntimeEngine.Workerd;
}
if (Is.stringValue(Runtime.processVersions()?.node)) {
return RuntimeEngine.Node;
}
if (
Is.object(NativeModules.getType("window")) &&
Is.object(NativeModules.getType("document"))
) {
return RuntimeEngine.Browser;
}
if (Is.function(NativeModules.getType("importScripts"))) {
return RuntimeEngine.WebWorker;
}
return RuntimeEngine.Unknown;
}

/**
* Get the version of the runtime engine.
* @returns The version, the user agent for browsers and workers, or an empty string if unknown.
*/
public static version(): string {
switch (Runtime.engine()) {
case RuntimeEngine.Bun:
return Runtime.processVersions()?.bun ?? "";
case RuntimeEngine.Deno:
return NativeModules.getType<{ version?: { deno?: string } }>("Deno")?.version?.deno ?? "";
case RuntimeEngine.Node:
return Runtime.processVersions()?.node ?? "";
case RuntimeEngine.Workerd:
case RuntimeEngine.Browser:
case RuntimeEngine.WebWorker:
return Runtime.userAgent() ?? "";
default:
return "";
}
}

/**
* Get the operating system.
* @returns The operating system.
*/
public static os(): RuntimeOs {
const name =
NativeModules.getType<{ build?: { os?: string } }>("Deno")?.build?.os ??
NativeModules.getType<{ platform?: string }>("process")?.platform ??
NativeModules.getType<{ userAgentData?: { platform?: string }; platform?: string }>(
"navigator"
)?.userAgentData?.platform ??
NativeModules.getType<{ platform?: string }>("navigator")?.platform ??
Runtime.userAgent() ??
"";

if (/^win|windows/i.test(name)) {
return RuntimeOs.Windows;
}
if (/^darwin|mac/i.test(name)) {
return RuntimeOs.Mac;
}
if (/linux/i.test(name)) {
return RuntimeOs.Linux;
}
return RuntimeOs.Unknown;
}

/**
* Get the process versions if there is a process global.
* @returns The versions, or undefined if there is no process global.
* @internal
*/
private static processVersions(): { [name: string]: string | undefined } | undefined {
return NativeModules.getType<{ versions?: { [name: string]: string | undefined } }>("process")
?.versions;
}

/**
* Get the navigator user agent if there is a navigator global.
* @returns The user agent, or undefined if there is no navigator global.
* @internal
*/
private static userAgent(): string | undefined {
return NativeModules.getType<{ userAgent?: string }>("navigator")?.userAgent;
}
}
103 changes: 103 additions & 0 deletions packages/core/tests/utils/runtime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2026 3Sixty Trade Ltd.
// SPDX-License-Identifier: Apache-2.0.
import { RuntimeEngine } from "../../src/models/runtimeEngine.js";
import { RuntimeOs } from "../../src/models/runtimeOs.js";
import { Runtime } from "../../src/utils/runtime.js";

/**
* Remove the server runtime globals so the browser and worker checks are reached.
*/
function stubNoServerRuntime(): void {
vi.stubGlobal("Deno", undefined);
vi.stubGlobal("process", undefined);
}

describe("Runtime", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

test("can detect the current engine", () => {
expect(Runtime.engine()).toEqual(
process.versions.bun === undefined ? RuntimeEngine.Node : RuntimeEngine.Bun
);
});

test("can get the current version", () => {
expect(Runtime.version()).toEqual(process.versions.bun ?? process.versions.node);
});

test("can detect the current os", () => {
const platforms: { [platform: string]: RuntimeOs } = {
win32: RuntimeOs.Windows,
darwin: RuntimeOs.Mac,
linux: RuntimeOs.Linux
};
expect(Runtime.os()).toEqual(platforms[process.platform] ?? RuntimeOs.Unknown);
});

test("can detect Bun", () => {
vi.stubGlobal("process", { versions: { node: "24.0.0", bun: "1.3.0" }, platform: "linux" });
expect(Runtime.engine()).toEqual(RuntimeEngine.Bun);
expect(Runtime.version()).toEqual("1.3.0");
expect(Runtime.os()).toEqual(RuntimeOs.Linux);
});

test("can detect Deno before the Node compatible process global", () => {
vi.stubGlobal("Deno", { version: { deno: "2.1.0" }, build: { os: "darwin" } });
vi.stubGlobal("process", { versions: { node: "22.0.0" }, platform: "linux" });
expect(Runtime.engine()).toEqual(RuntimeEngine.Deno);
expect(Runtime.version()).toEqual("2.1.0");
expect(Runtime.os()).toEqual(RuntimeOs.Mac);
});

test("can detect Cloudflare Workers", () => {
stubNoServerRuntime();
vi.stubGlobal("navigator", { userAgent: "Cloudflare-Workers" });
expect(Runtime.engine()).toEqual(RuntimeEngine.Workerd);
expect(Runtime.version()).toEqual("Cloudflare-Workers");
});

test("can detect Node", () => {
vi.stubGlobal("Deno", undefined);
vi.stubGlobal("process", { versions: { node: "24.1.0" }, platform: "win32" });
expect(Runtime.engine()).toEqual(RuntimeEngine.Node);
expect(Runtime.version()).toEqual("24.1.0");
expect(Runtime.os()).toEqual(RuntimeOs.Windows);
});

test("can detect a browser", () => {
stubNoServerRuntime();
vi.stubGlobal("window", {});
vi.stubGlobal("document", {});
vi.stubGlobal("navigator", {
userAgent: "Mozilla/5.0 (X11; Linux x86_64)",
userAgentData: { platform: "Windows" }
});
expect(Runtime.engine()).toEqual(RuntimeEngine.Browser);
expect(Runtime.version()).toEqual("Mozilla/5.0 (X11; Linux x86_64)");
expect(Runtime.os()).toEqual(RuntimeOs.Windows);
});

test("can detect a web worker", () => {
stubNoServerRuntime();
vi.stubGlobal("importScripts", () => {});
vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0", platform: "MacIntel" });
expect(Runtime.engine()).toEqual(RuntimeEngine.WebWorker);
expect(Runtime.os()).toEqual(RuntimeOs.Mac);
});

test("can detect the os from the user agent", () => {
stubNoServerRuntime();
vi.stubGlobal("navigator", { userAgent: "Mozilla/5.0 (X11; Linux x86_64)" });
expect(Runtime.os()).toEqual(RuntimeOs.Linux);
});

test("returns unknown when nothing can be detected", () => {
stubNoServerRuntime();
vi.stubGlobal("navigator", undefined);
expect(Runtime.engine()).toEqual(RuntimeEngine.Unknown);
expect(Runtime.version()).toEqual("");
expect(Runtime.os()).toEqual(RuntimeOs.Unknown);
});
});
7 changes: 4 additions & 3 deletions packages/crypto/tests/ciphers/chacha20poly1305.spec.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
// Copyright 2024 IOTA Stiftung.
// Copyright 2026 3Sixty Trade Ltd.
// SPDX-License-Identifier: Apache-2.0.
import { Converter, NativeModules } from "@3sixty/core";
import { Converter, NativeModules, Runtime, RuntimeEngine } from "@3sixty/core";
import testVectors from "./chacha20poly1305.json" with { type: "json" };
import { ChaCha20Poly1305 } from "../../src/ciphers/chaCha20Poly1305.js";
import {
IS_BUN,
NATIVE_CRYPTO_VARIANTS,
unregisterNodeCrypto,
useNativeCryptoVariant
} from "../nativeCryptoVariants.js";

// Bun does not provide chacha20-poly1305 natively, so only the pure JavaScript variant runs there
const variants = NATIVE_CRYPTO_VARIANTS.filter(variant => !IS_BUN || !variant.useNodeCrypto);
const variants = NATIVE_CRYPTO_VARIANTS.filter(
variant => Runtime.engine() !== RuntimeEngine.Bun || !variant.useNodeCrypto
);

describe.each(variants)("ChaCha20Poly1305 ($implementation)", ({ useNodeCrypto }) => {
beforeAll(async () => {
Expand Down
Loading
Loading