mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-26 06:52:57 +08:00
feat(sdk-node): add napi-rs TypeScript binding (@openshell/sdk)
Per RFC 0005, ship openshell-sdk-node as a napi-rs wrapper over the openshell-sdk crate, exposing the same surface to TypeScript/Node consumers as @openshell/sdk. - New crate openshell-sdk-node binds OpenShellClient, OidcRefresher, the edge-tunnel dialer, and the sandbox/exec API through napi-rs. - Generated index.d.ts and index.js are committed; the per-platform .node binary is gitignored and rebuilt with `napi build` per host. - lib.mjs provides a small ESM facade with named exports plus the errorCode() helper for typed error discrimination. Tests: 5-case smoke suite that exercises exports, typed connect errors, and OidcRefresher single-flight semantics.
This commit is contained in:
Generated
+104
-2
@@ -681,7 +681,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -841,6 +841,15 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
@@ -1045,6 +1054,12 @@ dependencies = [
|
||||
"rand_core 0.10.0-rc-3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d765eb1c0bda10d31e0ea185f5ee15da532d60b0912d2bd1441783439e749c5"
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
@@ -2825,6 +2840,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
@@ -3095,6 +3120,65 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1d395473824516f38dd1071a1a37bc57daa7be65b293ebba4ead5f7abb017a2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"futures",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
"rustc-hash 2.1.2",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "5.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eb602b84d7c1edae45e50bbf1374696548f36ae179dfa667f577e384bb90c2b"
|
||||
dependencies = [
|
||||
"libloading 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
@@ -3107,6 +3191,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -3548,7 +3638,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"futures",
|
||||
"libc",
|
||||
"libloading",
|
||||
"libloading 0.8.9",
|
||||
"miette",
|
||||
"nix",
|
||||
"oci-client",
|
||||
@@ -3688,6 +3778,18 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-sdk-node"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"openshell-sdk",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openshell-server"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
*.node
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "openshell-sdk-node"
|
||||
description = "napi-rs bindings for the OpenShell Rust SDK; published as @openshell/sdk"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
# Required for napi-rs: produces a dynamic library Node.js loads via N-API.
|
||||
# `rlib` is kept so the crate can also be exercised by `cargo test --workspace`.
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
openshell-sdk = { path = "../openshell-sdk" }
|
||||
async-trait = "0.1"
|
||||
napi = { version = "3", default-features = false, features = [
|
||||
"async",
|
||||
"napi6",
|
||||
"tokio_rt",
|
||||
"error_anyhow",
|
||||
] }
|
||||
napi-derive = "3"
|
||||
tokio = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2"
|
||||
|
||||
# Lints are disabled for this crate because napi-derive expands into patterns
|
||||
# (raw FFI signatures, Unsafe blocks for N-API, generated TypeScript shim
|
||||
# functions) that trigger several pedantic lints. The wrapped Rust SDK already
|
||||
# enforces the workspace lint policy.
|
||||
[lints]
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
}
|
||||
Vendored
+138
@@ -0,0 +1,138 @@
|
||||
/* auto-generated by NAPI-RS */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* A live token source backed by a JS callback. Hand off to
|
||||
* [`OpenShellClient::set_oidc_refresher`] before any RPCs run; the SDK
|
||||
* proactively refreshes when the token is within 60s of expiry, and
|
||||
* coalesces concurrent refreshes into a single callback invocation.
|
||||
*/
|
||||
export declare class OidcRefresher {
|
||||
/**
|
||||
* Create a refresher with an initial token and a JS callback.
|
||||
*
|
||||
* The callback must return a Promise resolving to
|
||||
* `{ accessToken, expiresAt? }`. `expiresAt` is Unix epoch seconds.
|
||||
*/
|
||||
constructor(initialToken: string, initialExpiresAt: number | undefined | null, callback: () => Promise<{ accessToken: string; expiresAt?: number }>)
|
||||
/**
|
||||
* Snapshot the current token (no refresh check). Mostly useful for
|
||||
* tests; in steady-state the SDK calls this internally.
|
||||
*/
|
||||
currentToken(): string
|
||||
/**
|
||||
* Force a refresh now and return the new access token. Concurrent
|
||||
* callers coalesce.
|
||||
*/
|
||||
refresh(): Promise<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* The JS-facing client. Cheap to share between async tasks; do not call
|
||||
* `connect` per request.
|
||||
*/
|
||||
export declare class OpenShellClient {
|
||||
/** Open a connection to the gateway described by `options`. */
|
||||
static connect(options: ConnectOptions): Promise<OpenShellClient>
|
||||
/** Gateway health snapshot. */
|
||||
health(): Promise<Health>
|
||||
/** Create a new sandbox. */
|
||||
createSandbox(spec: SandboxSpec): Promise<SandboxRef>
|
||||
/** Fetch a sandbox by name. */
|
||||
getSandbox(name: string): Promise<SandboxRef>
|
||||
/** List sandboxes. */
|
||||
listSandboxes(options?: ListOptions | undefined | null): Promise<Array<SandboxRef>>
|
||||
/**
|
||||
* Delete a sandbox by name. Returns `true` when the gateway acknowledged
|
||||
* the deletion, `false` when it was already absent.
|
||||
*/
|
||||
deleteSandbox(name: string): Promise<boolean>
|
||||
/** Poll until the sandbox reaches `ready` or `timeout_secs` elapses. */
|
||||
waitReady(name: string, timeoutSecs: number): Promise<SandboxRef>
|
||||
/** Poll until the sandbox is gone or `timeout_secs` elapses. */
|
||||
waitDeleted(name: string, timeoutSecs: number): Promise<void>
|
||||
/** Run a command inside a sandbox; buffers stdout/stderr to the end. */
|
||||
exec(name: string, command: Array<string>, options?: ExecOptions | undefined | null): Promise<ExecResult>
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection options. Mirrors [`openshell_sdk::ClientConfig`] with
|
||||
* JS-friendly field names.
|
||||
*/
|
||||
export interface ConnectOptions {
|
||||
/** Gateway URL (`http://...` or `https://...`). */
|
||||
gateway: string
|
||||
/** CA certificate (PEM-encoded). `None` falls back to system roots. */
|
||||
caCert?: Buffer
|
||||
/** Bearer token for direct OIDC auth. Mutually exclusive with `edge_token`. */
|
||||
oidcToken?: string
|
||||
/** Cloudflare Access bearer token. Routes through a local WebSocket tunnel. */
|
||||
edgeToken?: string
|
||||
/** Disable TLS certificate verification (development/debug only). */
|
||||
insecureSkipVerify?: boolean
|
||||
}
|
||||
|
||||
/** Options for [`OpenShellClient::exec`]. */
|
||||
export interface ExecOptions {
|
||||
workdir?: string
|
||||
environment?: Record<string, string>
|
||||
/** Timeout in seconds. `None` lets the gateway choose. */
|
||||
timeoutSecs?: number
|
||||
/** Optional stdin payload. */
|
||||
stdin?: Buffer
|
||||
}
|
||||
|
||||
/** Result of a non-streaming exec call. */
|
||||
export interface ExecResult {
|
||||
exitCode: number
|
||||
stdout: Buffer
|
||||
stderr: Buffer
|
||||
}
|
||||
|
||||
/** Gateway health snapshot. */
|
||||
export interface Health {
|
||||
/** Coarse status: `"healthy"`, `"degraded"`, `"unhealthy"`, `"unspecified"`. */
|
||||
status: string
|
||||
version: string
|
||||
}
|
||||
|
||||
/** JS-side refresh callback returning a Promise<{ accessToken, expiresAt? }>. */
|
||||
export interface JsRefreshedToken {
|
||||
accessToken: string
|
||||
/**
|
||||
* Expiry as Unix epoch seconds. Stored as `f64` because JS numbers
|
||||
* can't hold `u64` exactly past 2^53; values are clamped to that range
|
||||
* in practice (the year 287396 is fine).
|
||||
*/
|
||||
expiresAt?: number
|
||||
}
|
||||
|
||||
/** Options for [`OpenShellClient::list_sandboxes`]. */
|
||||
export interface ListOptions {
|
||||
limit?: number
|
||||
offset?: number
|
||||
labelSelector?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle phase: `"unspecified"`, `"provisioning"`, `"ready"`, `"error"`,
|
||||
* `"deleting"`, `"unknown"`.
|
||||
*/
|
||||
export interface SandboxRef {
|
||||
id: string
|
||||
name: string
|
||||
phase: string
|
||||
labels: Record<string, string>
|
||||
/** Resource version as a string — JS numbers can't safely hold u64. */
|
||||
resourceVersion: string
|
||||
}
|
||||
|
||||
/** Caller intent for a new sandbox. */
|
||||
export interface SandboxSpec {
|
||||
name?: string
|
||||
image?: string
|
||||
labels?: Record<string, string>
|
||||
environment?: Record<string, string>
|
||||
providers?: Array<string>
|
||||
gpu?: boolean
|
||||
gpuDevice?: string
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const { readFileSync } = require('node:fs')
|
||||
let nativeBinding = null
|
||||
const loadErrors = []
|
||||
|
||||
const isMusl = () => {
|
||||
let musl = false
|
||||
if (process.platform === 'linux') {
|
||||
musl = isMuslFromFilesystem()
|
||||
if (musl === null) {
|
||||
musl = isMuslFromReport()
|
||||
}
|
||||
if (musl === null) {
|
||||
musl = isMuslFromChildProcess()
|
||||
}
|
||||
}
|
||||
return musl
|
||||
}
|
||||
|
||||
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
|
||||
|
||||
const isMuslFromFilesystem = () => {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isMuslFromReport = () => {
|
||||
let report = null
|
||||
if (typeof process.report?.getReport === 'function') {
|
||||
process.report.excludeNetwork = true
|
||||
report = process.report.getReport()
|
||||
}
|
||||
if (!report) {
|
||||
return null
|
||||
}
|
||||
if (report.header && report.header.glibcVersionRuntime) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(report.sharedObjects)) {
|
||||
if (report.sharedObjects.some(isFileMusl)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isMuslFromChildProcess = () => {
|
||||
try {
|
||||
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
|
||||
} catch (e) {
|
||||
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function requireNative() {
|
||||
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||
try {
|
||||
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
} else if (process.platform === 'android') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./openshell-sdk.android-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-android-arm64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-android-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./openshell-sdk.android-arm-eabi.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-android-arm-eabi')
|
||||
const bindingPackageVersion = require('@openshell/sdk-android-arm-eabi/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (process.arch === 'x64') {
|
||||
if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') {
|
||||
try {
|
||||
return require('./openshell-sdk.win32-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-win32-x64-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-win32-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./openshell-sdk.win32-x64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-win32-x64-msvc')
|
||||
const bindingPackageVersion = require('@openshell/sdk-win32-x64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ia32') {
|
||||
try {
|
||||
return require('./openshell-sdk.win32-ia32-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-win32-ia32-msvc')
|
||||
const bindingPackageVersion = require('@openshell/sdk-win32-ia32-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./openshell-sdk.win32-arm64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-win32-arm64-msvc')
|
||||
const bindingPackageVersion = require('@openshell/sdk-win32-arm64-msvc/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
try {
|
||||
return require('./openshell-sdk.darwin-universal.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-darwin-universal')
|
||||
const bindingPackageVersion = require('@openshell/sdk-darwin-universal/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./openshell-sdk.darwin-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-darwin-x64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-darwin-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./openshell-sdk.darwin-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-darwin-arm64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-darwin-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'freebsd') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./openshell-sdk.freebsd-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-freebsd-x64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-freebsd-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./openshell-sdk.freebsd-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-freebsd-arm64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-freebsd-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (process.arch === 'x64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-x64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-x64-musl')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-x64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-x64-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-x64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-arm64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-arm64-musl')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-arm64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-arm64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-arm64-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-arm64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-arm-musleabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-arm-musleabihf')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-arm-musleabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-arm-gnueabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-arm-gnueabihf')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-arm-gnueabihf/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'loong64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-loong64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-loong64-musl')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-loong64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-loong64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-loong64-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-loong64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'riscv64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-riscv64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-riscv64-musl')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-riscv64-musl/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-riscv64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-riscv64-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-riscv64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ppc64') {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-ppc64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-ppc64-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-ppc64-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 's390x') {
|
||||
try {
|
||||
return require('./openshell-sdk.linux-s390x-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-linux-s390x-gnu')
|
||||
const bindingPackageVersion = require('@openshell/sdk-linux-s390x-gnu/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'openharmony') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./openshell-sdk.openharmony-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-openharmony-arm64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-openharmony-arm64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./openshell-sdk.openharmony-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-openharmony-x64')
|
||||
const bindingPackageVersion = require('@openshell/sdk-openharmony-x64/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./openshell-sdk.openharmony-arm.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@openshell/sdk-openharmony-arm')
|
||||
const bindingPackageVersion = require('@openshell/sdk-openharmony-arm/package.json').version
|
||||
if (bindingPackageVersion !== '0.0.0-alpha.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
|
||||
throw new Error(`Native binding package version mismatch, expected 0.0.0-alpha.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
|
||||
}
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
|
||||
}
|
||||
}
|
||||
|
||||
nativeBinding = requireNative()
|
||||
|
||||
// NAPI_RS_FORCE_WASI is a tri-state flag:
|
||||
// unset / any other value → native binding preferred, WASI is only a fallback
|
||||
// 'true' → force WASI fallback even if native loaded
|
||||
// 'error' → force WASI and throw if no WASI binding is found
|
||||
// Treating any non-empty string as truthy (the historical behavior) meant
|
||||
// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered
|
||||
// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file.
|
||||
const forceWasi =
|
||||
process.env.NAPI_RS_FORCE_WASI === 'true' || process.env.NAPI_RS_FORCE_WASI === 'error'
|
||||
|
||||
if (!nativeBinding || forceWasi) {
|
||||
let wasiBinding = null
|
||||
let wasiBindingError = null
|
||||
try {
|
||||
wasiBinding = require('./openshell-sdk.wasi.cjs')
|
||||
nativeBinding = wasiBinding
|
||||
} catch (err) {
|
||||
if (forceWasi) {
|
||||
wasiBindingError = err
|
||||
}
|
||||
}
|
||||
if (!nativeBinding || forceWasi) {
|
||||
try {
|
||||
wasiBinding = require('@openshell/sdk-wasm32-wasi')
|
||||
nativeBinding = wasiBinding
|
||||
} catch (err) {
|
||||
if (forceWasi) {
|
||||
if (!wasiBindingError) {
|
||||
wasiBindingError = err
|
||||
} else {
|
||||
wasiBindingError.cause = err
|
||||
}
|
||||
loadErrors.push(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) {
|
||||
const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error')
|
||||
error.cause = wasiBindingError
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Cannot find native binding. ` +
|
||||
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
|
||||
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
|
||||
{
|
||||
cause: loadErrors.reduce((err, cur) => {
|
||||
cur.cause = err
|
||||
return cur
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
module.exports = nativeBinding
|
||||
module.exports.OidcRefresher = nativeBinding.OidcRefresher
|
||||
module.exports.OpenShellClient = nativeBinding.OpenShellClient
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { OidcRefresher, OpenShellClient } from './index.js'
|
||||
export type {
|
||||
ConnectOptions,
|
||||
ExecOptions,
|
||||
ExecResult,
|
||||
Health,
|
||||
JsRefreshedToken,
|
||||
ListOptions,
|
||||
SandboxRef,
|
||||
SandboxSpec,
|
||||
} from './index.js'
|
||||
|
||||
/**
|
||||
* Extract the SDK error code from a thrown error.
|
||||
*
|
||||
* The native binding prefixes every error message with `[code] ` where
|
||||
* `code` is one of: `invalid_config`, `tls`, `connect`, `auth`, `io`,
|
||||
* `not_found`, `already_exists`, `rpc`. Returns `null` when the prefix is
|
||||
* missing (the error wasn't thrown by this binding).
|
||||
*/
|
||||
export declare function errorCode(err: unknown): string | null
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// ESM facade over the auto-generated CommonJS index.
|
||||
//
|
||||
// Re-exports the napi-generated classes and adds the `errorCode` helper,
|
||||
// which parses the `[code] message` prefix the binding uses to surface the
|
||||
// SDK's discriminable error kind. The auto-generated `index.js`/`index.d.ts`
|
||||
// pair from napi-rs is left untouched so the build matrix stays cookie-cutter.
|
||||
|
||||
import nativeBinding from './index.js'
|
||||
|
||||
const { OpenShellClient, OidcRefresher } = nativeBinding
|
||||
|
||||
export { OpenShellClient, OidcRefresher }
|
||||
|
||||
/**
|
||||
* Extract the SDK error code from a thrown error.
|
||||
*
|
||||
* The native binding prefixes every error message with `[code] ` where
|
||||
* `code` is one of: `invalid_config`, `tls`, `connect`, `auth`, `io`,
|
||||
* `not_found`, `already_exists`, `rpc`. Returns `null` when the prefix is
|
||||
* missing (the error wasn't thrown by this binding).
|
||||
*
|
||||
* @param {unknown} err
|
||||
* @returns {string | null}
|
||||
*/
|
||||
export function errorCode(err) {
|
||||
if (!err || typeof err.message !== 'string') return null
|
||||
const match = err.message.match(/^\[([^\]]+)\]/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@openshell/sdk",
|
||||
"version": "0.0.0-alpha.0",
|
||||
"description": "TypeScript SDK for OpenShell gateways. Wraps the openshell-sdk Rust core via napi-rs.",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./lib.d.ts",
|
||||
"default": "./lib.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./index.d.ts",
|
||||
"default": "./index.js"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/NVIDIA/OpenShell.git",
|
||||
"directory": "crates/openshell-sdk-node"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"lib.mjs",
|
||||
"lib.d.ts"
|
||||
],
|
||||
"napi": {
|
||||
"binaryName": "openshell-sdk",
|
||||
"targets": [
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
"build:debug": "napi build --platform",
|
||||
"test": "node test/smoke.mjs",
|
||||
"prepublishOnly": "napi prepublish -t npm"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! napi-rs bindings over [`openshell_sdk`].
|
||||
//!
|
||||
//! This crate is a thin adapter — it owns no business logic. It maps the
|
||||
//! curated SDK surface to JS-shaped types (camelCase keys, string-literal
|
||||
//! enums, JS `Error` with a discriminable `code` field), and bridges a
|
||||
//! JS-side OIDC refresh callback to the SDK's [`openshell_sdk::Refresh`]
|
||||
//! trait.
|
||||
//!
|
||||
//! Published as `@openshell/sdk` (alpha; no semver guarantee until 1.0).
|
||||
//!
|
||||
//! # Runtime ownership
|
||||
//!
|
||||
//! napi-rs v3 provides an ambient tokio runtime that's only available inside
|
||||
//! `async fn` napi entry points. Every JS-facing function on [`OpenShellClient`]
|
||||
//! is therefore `async`. Sync FFI entry points cannot call the SDK because
|
||||
//! tonic requires a reactor; attempting `tokio::spawn` from a sync `#[napi]`
|
||||
//! function panics with "no reactor running".
|
||||
|
||||
#![allow(clippy::needless_pass_by_value, clippy::missing_errors_doc)]
|
||||
|
||||
use napi::Status;
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi::threadsafe_function::ThreadsafeFunction;
|
||||
use napi_derive::napi;
|
||||
use openshell_sdk as sdk;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
// ── Error mapping ─────────────────────────────────────────────────────
|
||||
|
||||
fn to_napi_error(error: sdk::SdkError) -> Error {
|
||||
let code = error.code();
|
||||
// Embed the SDK code as a `[code] message` prefix. N-API maps the
|
||||
// status enum to JS `err.code`, which is too coarse for discrimination
|
||||
// (`GenericFailure` covers most variants), so callers parse the prefix
|
||||
// or use the `errorCode()` helper exported from the JS shim.
|
||||
Error::new(Status::GenericFailure, format!("[{code}] {error}"))
|
||||
}
|
||||
|
||||
// ── Public input/output types (JS-shaped) ────────────────────────────
|
||||
|
||||
/// Connection options. Mirrors [`openshell_sdk::ClientConfig`] with
|
||||
/// JS-friendly field names.
|
||||
#[napi(object)]
|
||||
#[derive(Default)]
|
||||
pub struct ConnectOptions {
|
||||
/// Gateway URL (`http://...` or `https://...`).
|
||||
pub gateway: String,
|
||||
/// CA certificate (PEM-encoded). `None` falls back to system roots.
|
||||
pub ca_cert: Option<Buffer>,
|
||||
/// Bearer token for direct OIDC auth. Mutually exclusive with `edge_token`.
|
||||
pub oidc_token: Option<String>,
|
||||
/// Cloudflare Access bearer token. Routes through a local WebSocket tunnel.
|
||||
pub edge_token: Option<String>,
|
||||
/// Disable TLS certificate verification (development/debug only).
|
||||
pub insecure_skip_verify: Option<bool>,
|
||||
}
|
||||
|
||||
/// Gateway health snapshot.
|
||||
#[napi(object)]
|
||||
pub struct Health {
|
||||
/// Coarse status: `"healthy"`, `"degraded"`, `"unhealthy"`, `"unspecified"`.
|
||||
pub status: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// Lifecycle phase: `"unspecified"`, `"provisioning"`, `"ready"`, `"error"`,
|
||||
/// `"deleting"`, `"unknown"`.
|
||||
#[napi(object)]
|
||||
pub struct SandboxRef {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub phase: String,
|
||||
pub labels: HashMap<String, String>,
|
||||
/// Resource version as a string — JS numbers can't safely hold u64.
|
||||
pub resource_version: String,
|
||||
}
|
||||
|
||||
/// Caller intent for a new sandbox.
|
||||
#[napi(object)]
|
||||
#[derive(Default)]
|
||||
pub struct SandboxSpec {
|
||||
pub name: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub labels: Option<HashMap<String, String>>,
|
||||
pub environment: Option<HashMap<String, String>>,
|
||||
pub providers: Option<Vec<String>>,
|
||||
pub gpu: Option<bool>,
|
||||
pub gpu_device: Option<String>,
|
||||
}
|
||||
|
||||
/// Options for [`OpenShellClient::list_sandboxes`].
|
||||
#[napi(object)]
|
||||
#[derive(Default)]
|
||||
pub struct ListOptions {
|
||||
pub limit: Option<u32>,
|
||||
pub offset: Option<u32>,
|
||||
pub label_selector: Option<String>,
|
||||
}
|
||||
|
||||
/// Options for [`OpenShellClient::exec`].
|
||||
#[napi(object)]
|
||||
#[derive(Default)]
|
||||
pub struct ExecOptions {
|
||||
pub workdir: Option<String>,
|
||||
pub environment: Option<HashMap<String, String>>,
|
||||
/// Timeout in seconds. `None` lets the gateway choose.
|
||||
pub timeout_secs: Option<u32>,
|
||||
/// Optional stdin payload.
|
||||
pub stdin: Option<Buffer>,
|
||||
}
|
||||
|
||||
/// Result of a non-streaming exec call.
|
||||
#[napi(object)]
|
||||
pub struct ExecResult {
|
||||
pub exit_code: i32,
|
||||
pub stdout: Buffer,
|
||||
pub stderr: Buffer,
|
||||
}
|
||||
|
||||
// ── Type conversions ─────────────────────────────────────────────────
|
||||
|
||||
fn phase_to_str(phase: sdk::SandboxPhase) -> &'static str {
|
||||
match phase {
|
||||
sdk::SandboxPhase::Provisioning => "provisioning",
|
||||
sdk::SandboxPhase::Ready => "ready",
|
||||
sdk::SandboxPhase::Error => "error",
|
||||
sdk::SandboxPhase::Deleting => "deleting",
|
||||
sdk::SandboxPhase::Unknown => "unknown",
|
||||
_ => "unspecified",
|
||||
}
|
||||
}
|
||||
|
||||
fn status_to_str(status: sdk::ServiceStatus) -> &'static str {
|
||||
match status {
|
||||
sdk::ServiceStatus::Healthy => "healthy",
|
||||
sdk::ServiceStatus::Degraded => "degraded",
|
||||
sdk::ServiceStatus::Unhealthy => "unhealthy",
|
||||
_ => "unspecified",
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sdk::SandboxRef> for SandboxRef {
|
||||
fn from(r: sdk::SandboxRef) -> Self {
|
||||
Self {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
phase: phase_to_str(r.phase).to_string(),
|
||||
labels: r.labels,
|
||||
resource_version: r.resource_version.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sdk_spec_from_js(spec: SandboxSpec) -> sdk::SandboxSpec {
|
||||
sdk::SandboxSpec {
|
||||
name: spec.name,
|
||||
image: spec.image,
|
||||
labels: spec.labels.unwrap_or_default(),
|
||||
environment: spec.environment.unwrap_or_default(),
|
||||
providers: spec.providers.unwrap_or_default(),
|
||||
gpu: spec.gpu.unwrap_or(false),
|
||||
gpu_device: spec.gpu_device,
|
||||
}
|
||||
}
|
||||
|
||||
fn sdk_list_opts_from_js(opts: ListOptions) -> sdk::ListOptions {
|
||||
sdk::ListOptions {
|
||||
limit: opts.limit.unwrap_or(0),
|
||||
offset: opts.offset.unwrap_or(0),
|
||||
label_selector: opts.label_selector,
|
||||
}
|
||||
}
|
||||
|
||||
fn sdk_exec_opts_from_js(opts: ExecOptions) -> sdk::ExecOptions {
|
||||
sdk::ExecOptions {
|
||||
workdir: opts.workdir,
|
||||
environment: opts.environment.unwrap_or_default(),
|
||||
timeout: opts.timeout_secs.map(|s| Duration::from_secs(u64::from(s))),
|
||||
stdin: opts.stdin.map(|b| b.to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_client_config(opts: ConnectOptions) -> sdk::ClientConfig {
|
||||
let auth = match (opts.oidc_token, opts.edge_token) {
|
||||
(Some(token), _) => Some(sdk::AuthConfig::Oidc(token)),
|
||||
(None, Some(token)) => Some(sdk::AuthConfig::EdgeJwt(token)),
|
||||
(None, None) => None,
|
||||
};
|
||||
let mut cfg = sdk::ClientConfig::new(opts.gateway);
|
||||
cfg.ca_cert = opts.ca_cert.map(|b| b.to_vec());
|
||||
cfg.auth = auth;
|
||||
cfg.insecure_skip_verify = opts.insecure_skip_verify.unwrap_or(false);
|
||||
cfg
|
||||
}
|
||||
|
||||
// ── OIDC refresh callback bridge ─────────────────────────────────────
|
||||
|
||||
/// JS-side refresh callback returning a Promise<{ accessToken, expiresAt? }>.
|
||||
#[napi(object)]
|
||||
pub struct JsRefreshedToken {
|
||||
pub access_token: String,
|
||||
/// Expiry as Unix epoch seconds. Stored as `f64` because JS numbers
|
||||
/// can't hold `u64` exactly past 2^53; values are clamped to that range
|
||||
/// in practice (the year 287396 is fine).
|
||||
pub expires_at: Option<f64>,
|
||||
}
|
||||
|
||||
/// Bridge between a JS refresh callback and the SDK's [`sdk::Refresh`] trait.
|
||||
struct JsRefresher {
|
||||
callback: ThreadsafeFunction<(), Promise<JsRefreshedToken>, (), Status, false>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl sdk::Refresh for JsRefresher {
|
||||
async fn refresh(&self) -> std::result::Result<sdk::RefreshedToken, sdk::RefreshError> {
|
||||
// Invoke the JS callback; it returns a Promise<JsRefreshedToken>.
|
||||
let promise =
|
||||
self.callback.call_async(()).await.map_err(|e| {
|
||||
sdk::RefreshError::Transient(format!("refresh callback failed: {e}"))
|
||||
})?;
|
||||
let result = promise
|
||||
.await
|
||||
.map_err(|e| sdk::RefreshError::Transient(format!("refresh promise rejected: {e}")))?;
|
||||
let token = sdk::RefreshedToken::new(result.access_token);
|
||||
Ok(match result.expires_at {
|
||||
Some(expires_at) if expires_at.is_finite() && expires_at > 0.0 => {
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
{
|
||||
token.with_expires_at(expires_at as u64)
|
||||
}
|
||||
}
|
||||
_ => token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A live token source backed by a JS callback. Hand off to
|
||||
/// [`OpenShellClient::set_oidc_refresher`] before any RPCs run; the SDK
|
||||
/// proactively refreshes when the token is within 60s of expiry, and
|
||||
/// coalesces concurrent refreshes into a single callback invocation.
|
||||
#[napi]
|
||||
pub struct OidcRefresher {
|
||||
inner: Arc<sdk::TokenSource>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OidcRefresher {
|
||||
/// Create a refresher with an initial token and a JS callback.
|
||||
///
|
||||
/// The callback must return a Promise resolving to
|
||||
/// `{ accessToken, expiresAt? }`. `expiresAt` is Unix epoch seconds.
|
||||
#[napi(constructor)]
|
||||
pub fn new(
|
||||
initial_token: String,
|
||||
initial_expires_at: Option<f64>,
|
||||
#[napi(ts_arg_type = "() => Promise<{ accessToken: string; expiresAt?: number }>")]
|
||||
callback: ThreadsafeFunction<(), Promise<JsRefreshedToken>, (), Status, false>,
|
||||
) -> Self {
|
||||
let initial = match initial_expires_at {
|
||||
Some(exp) if exp.is_finite() && exp > 0.0 => {
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
{
|
||||
sdk::RefreshedToken::new(initial_token).with_expires_at(exp as u64)
|
||||
}
|
||||
}
|
||||
_ => sdk::RefreshedToken::new(initial_token),
|
||||
};
|
||||
let refresher = Arc::new(JsRefresher { callback });
|
||||
Self {
|
||||
inner: Arc::new(sdk::TokenSource::new(initial, refresher)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the current token (no refresh check). Mostly useful for
|
||||
/// tests; in steady-state the SDK calls this internally.
|
||||
#[napi]
|
||||
pub fn current_token(&self) -> String {
|
||||
self.inner.snapshot()
|
||||
}
|
||||
|
||||
/// Force a refresh now and return the new access token. Concurrent
|
||||
/// callers coalesce.
|
||||
#[napi]
|
||||
pub async fn refresh(&self) -> Result<String> {
|
||||
self.inner.refresh_now().await.map_err(to_napi_error)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main client class ────────────────────────────────────────────────
|
||||
|
||||
/// The JS-facing client. Cheap to share between async tasks; do not call
|
||||
/// `connect` per request.
|
||||
#[napi]
|
||||
pub struct OpenShellClient {
|
||||
inner: sdk::OpenShellClient,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OpenShellClient {
|
||||
/// Open a connection to the gateway described by `options`.
|
||||
#[napi(factory)]
|
||||
pub async fn connect(options: ConnectOptions) -> Result<Self> {
|
||||
let cfg = build_client_config(options);
|
||||
let inner = sdk::OpenShellClient::connect(cfg)
|
||||
.await
|
||||
.map_err(to_napi_error)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/// Gateway health snapshot.
|
||||
#[napi]
|
||||
pub async fn health(&self) -> Result<Health> {
|
||||
let h = self.inner.health().await.map_err(to_napi_error)?;
|
||||
Ok(Health {
|
||||
status: status_to_str(h.status).to_string(),
|
||||
version: h.version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new sandbox.
|
||||
#[napi]
|
||||
pub async fn create_sandbox(&self, spec: SandboxSpec) -> Result<SandboxRef> {
|
||||
self.inner
|
||||
.create_sandbox(sdk_spec_from_js(spec))
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(to_napi_error)
|
||||
}
|
||||
|
||||
/// Fetch a sandbox by name.
|
||||
#[napi]
|
||||
pub async fn get_sandbox(&self, name: String) -> Result<SandboxRef> {
|
||||
self.inner
|
||||
.get_sandbox(&name)
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(to_napi_error)
|
||||
}
|
||||
|
||||
/// List sandboxes.
|
||||
#[napi]
|
||||
pub async fn list_sandboxes(&self, options: Option<ListOptions>) -> Result<Vec<SandboxRef>> {
|
||||
let opts = sdk_list_opts_from_js(options.unwrap_or_default());
|
||||
let items = self
|
||||
.inner
|
||||
.list_sandboxes(opts)
|
||||
.await
|
||||
.map_err(to_napi_error)?;
|
||||
Ok(items.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
/// Delete a sandbox by name. Returns `true` when the gateway acknowledged
|
||||
/// the deletion, `false` when it was already absent.
|
||||
#[napi]
|
||||
pub async fn delete_sandbox(&self, name: String) -> Result<bool> {
|
||||
self.inner
|
||||
.delete_sandbox(&name)
|
||||
.await
|
||||
.map_err(to_napi_error)
|
||||
}
|
||||
|
||||
/// Poll until the sandbox reaches `ready` or `timeout_secs` elapses.
|
||||
#[napi]
|
||||
pub async fn wait_ready(&self, name: String, timeout_secs: u32) -> Result<SandboxRef> {
|
||||
self.inner
|
||||
.wait_ready(&name, Duration::from_secs(u64::from(timeout_secs)))
|
||||
.await
|
||||
.map(Into::into)
|
||||
.map_err(to_napi_error)
|
||||
}
|
||||
|
||||
/// Poll until the sandbox is gone or `timeout_secs` elapses.
|
||||
#[napi]
|
||||
pub async fn wait_deleted(&self, name: String, timeout_secs: u32) -> Result<()> {
|
||||
self.inner
|
||||
.wait_deleted(&name, Duration::from_secs(u64::from(timeout_secs)))
|
||||
.await
|
||||
.map_err(to_napi_error)
|
||||
}
|
||||
|
||||
/// Run a command inside a sandbox; buffers stdout/stderr to the end.
|
||||
#[napi]
|
||||
pub async fn exec(
|
||||
&self,
|
||||
name: String,
|
||||
command: Vec<String>,
|
||||
options: Option<ExecOptions>,
|
||||
) -> Result<ExecResult> {
|
||||
let opts = sdk_exec_opts_from_js(options.unwrap_or_default());
|
||||
let res = self
|
||||
.inner
|
||||
.exec(&name, &command, opts)
|
||||
.await
|
||||
.map_err(to_napi_error)?;
|
||||
Ok(ExecResult {
|
||||
exit_code: res.exit_code,
|
||||
stdout: res.stdout.into(),
|
||||
stderr: res.stderr.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Smoke test for @openshell/sdk.
|
||||
//
|
||||
// Verifies the binding's surface: exports, error mapping, and the
|
||||
// `OidcRefresher` single-flight contract. End-to-end RPC verification
|
||||
// requires a running mock gateway and lives in the Rust crate's mock tests
|
||||
// (see `crates/openshell-sdk/tests/client_mock.rs`).
|
||||
//
|
||||
// Implemented as a plain async script rather than `node --test` because
|
||||
// the napi-rs `tokio_rt` feature holds the libuv event loop open and the
|
||||
// test runner never exits cleanly. We explicitly `process.exit(0)` here.
|
||||
|
||||
import { strict as assert } from 'node:assert'
|
||||
import { errorCode, OidcRefresher, OpenShellClient } from '../lib.mjs'
|
||||
|
||||
const cases = []
|
||||
function test(name, fn) {
|
||||
cases.push({ name, fn })
|
||||
}
|
||||
|
||||
test('module exports the documented classes', () => {
|
||||
assert.equal(typeof OpenShellClient, 'function')
|
||||
assert.equal(typeof OpenShellClient.connect, 'function')
|
||||
assert.equal(typeof OidcRefresher, 'function')
|
||||
assert.equal(typeof errorCode, 'function')
|
||||
})
|
||||
|
||||
test('connect rejects against a closed port with a typed error', async () => {
|
||||
let caught
|
||||
try {
|
||||
await OpenShellClient.connect({ gateway: 'http://127.0.0.1:1' })
|
||||
} catch (err) {
|
||||
caught = err
|
||||
}
|
||||
assert.ok(caught, 'expected connect to fail against 127.0.0.1:1')
|
||||
assert.equal(errorCode(caught), 'connect', `unexpected error: ${caught?.message}`)
|
||||
})
|
||||
|
||||
test('connect rejects with invalid_config for a malformed gateway URL', async () => {
|
||||
let caught
|
||||
try {
|
||||
await OpenShellClient.connect({ gateway: 'not-a-url' })
|
||||
} catch (err) {
|
||||
caught = err
|
||||
}
|
||||
assert.ok(caught, 'expected connect to fail on malformed URL')
|
||||
assert.equal(errorCode(caught), 'invalid_config', `unexpected error: ${caught?.message}`)
|
||||
})
|
||||
|
||||
test('OidcRefresher coalesces concurrent refresh calls', async () => {
|
||||
let calls = 0
|
||||
// expiresAt = 1 (Unix epoch 1970) is in the past, so the SDK's reactive
|
||||
// path treats the token as expired. See refresh.rs `needs_refresh`.
|
||||
const refresher = new OidcRefresher('initial', 1, async () => {
|
||||
calls += 1
|
||||
await new Promise((resolve) => setTimeout(resolve, 25))
|
||||
return {
|
||||
accessToken: `token-${calls}`,
|
||||
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
||||
}
|
||||
})
|
||||
|
||||
const results = await Promise.all([
|
||||
refresher.refresh(),
|
||||
refresher.refresh(),
|
||||
refresher.refresh(),
|
||||
refresher.refresh(),
|
||||
])
|
||||
|
||||
assert.equal(calls, 1, 'callback should have been invoked once for coalesced calls')
|
||||
assert.equal(new Set(results).size, 1, 'all waiters should observe the same token')
|
||||
assert.equal(results[0], 'token-1')
|
||||
assert.equal(refresher.currentToken(), 'token-1')
|
||||
})
|
||||
|
||||
test('OidcRefresher surfaces callback rejections as auth errors', async () => {
|
||||
const refresher = new OidcRefresher('stale', 1, async () => {
|
||||
throw new Error('IdP unreachable')
|
||||
})
|
||||
|
||||
let caught
|
||||
try {
|
||||
await refresher.refresh()
|
||||
} catch (err) {
|
||||
caught = err
|
||||
}
|
||||
assert.ok(caught, 'expected refresh to reject when callback throws')
|
||||
assert.equal(errorCode(caught), 'auth', `unexpected error: ${caught?.message}`)
|
||||
})
|
||||
|
||||
let failed = 0
|
||||
for (const { name, fn } of cases) {
|
||||
try {
|
||||
await fn()
|
||||
console.log(`ok ${name}`)
|
||||
} catch (err) {
|
||||
failed += 1
|
||||
console.error(`fail ${name}`)
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
console.log(`\n${cases.length - failed}/${cases.length} passed`)
|
||||
process.exit(failed === 0 ? 0 : 1)
|
||||
Reference in New Issue
Block a user