diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 7f697731..0104ae70 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -19,9 +19,11 @@ flowchart TD
T4 --> TB["Patched Node.js binary
with custom VFS"]
S1 --> S2["Asset blob + manifest"]
- S2 --> S3["node --experimental-sea-config"]
- S3 --> S4["postject inject"]
- S4 --> SB["Stock Node.js binary
with NODE_SEA_BLOB"]
+ S2 --> S3{"generator Node
>= 25.5?"}
+ S3 -- ">= 25.5" --> S3a["node --build-sea
(in-core LIEF)"]
+ S3 -- "< 25.5" --> S3b["node --experimental-sea-config
+ postject inject"]
+ S3a --> SB["Node.js binary
with NODE_SEA_BLOB"]
+ S3b --> SB
style P stroke:#e89b2c,stroke-width:2px
style TB stroke:#ec7a96,stroke-width:2px
@@ -204,17 +206,20 @@ flowchart TD
SEA[sea.ts seaEnhanced]
CONFIG[Build sea-config.json]
BOOT[sea-bootstrap.bundle.js
pre-bundled by esbuild]
+ INJ{generator Node >= 25.5?}
+ BUILDSEA[node --build-sea
generate blob + inject in-core]
BLOB[node --experimental-sea-config
produces prep blob]
- INJ[postject
inject NODE_SEA_BLOB]
- OUT[Stock Node.js executable
+ NODE_SEA_BLOB resource]
+ POST[postject
inject NODE_SEA_BLOB]
+ OUT[Node.js executable
+ NODE_SEA_BLOB resource]
CLI --> DETECT
DETECT -- enhanced --> WALK
- DETECT -- simple single-file --> BLOB
+ DETECT -- simple single-file --> CONFIG
WALK --> ASSETS --> SEA
- SEA --> CONFIG --> BLOB
- SEA --> BOOT --> BLOB
- BLOB --> INJ --> OUT
+ SEA --> CONFIG --> INJ
+ SEA --> BOOT --> CONFIG
+ INJ -- ">= 25.5" --> BUILDSEA --> OUT
+ INJ -- "< 25.5" --> BLOB --> POST --> OUT
style CLI stroke:#e89b2c,stroke-width:2px
style OUT stroke:#66bb6a,stroke-width:2px
@@ -256,18 +261,38 @@ CLI (lib/index.ts)
├─ Pick blob generator binary (host/target major):
│ host major === target major → process.execPath
│ otherwise → downloaded target binary
- ├─ Generate blob:
- │ node --experimental-sea-config sea-config.json
- │ (--build-sea is intentionally NOT used — it produces a
- │ finished executable and bypasses the prep-blob + postject
- │ flow that pkg needs for multi-target injection)
- ├─ For each target:
- │ 1. Download Node.js binary (getNodejsExecutable)
- │ 2. Inject blob via postject (bake)
- │ 3. Sign macOS if needed (signMacOSIfNeeded)
+ ├─ injectAllTargets() — choose injection path by generator version:
+ │
+ │ • Generator Node >= 25.5 → in-core `node --build-sea`:
+ │ For each target, write a per-target config adding
+ │ { executable: , output: }
+ │ then run `node --build-sea config.json`. Node generates the
+ │ blob AND injects it (its bundled, current LIEF) in one step.
+ │ `executable` lets us point at any per-target base binary, and
+ │ assertSingleTargetMajor() keeps every target on the generator's
+ │ major, so the generated blob is valid for all of them — i.e. the
+ │ multi-target concern that previously ruled --build-sea out does
+ │ not actually apply. This is preferred because Node's bundled LIEF
+ │ is current; postject's vendored LIEF (0.13) corrupts the dynamic
+ │ symbol table of PIE/ET_DYN bases and breaks native addons.
+ │
+ │ • Generator Node < 25.5 → classic prep-blob + postject:
+ │ 1. node --experimental-sea-config sea-config.json (one blob)
+ │ 2. For each target: copy base binary, inject blob via postject
+ │ (bake), sign macOS if needed.
└─ Cleanup tmpDir
```
+> **Note on `--build-sea`:** earlier versions of pkg deliberately avoided
+> `node --build-sea`, on the assumption that it produced a finished executable
+> from the _running_ Node and so couldn't serve pkg's prep-blob + per-target
+> injection model. That turns out not to hold: `--build-sea` reads the base
+> binary from the config's `executable` field (defaulting to the running Node
+> only when omitted) and writes to `output`, so a single host-runnable Node
+>
+> > = 25.5 can inject into each target's base binary directly. pkg now uses it
+> > whenever the generator supports it, and falls back to postject otherwise.
+
### SEA Binary Format
The SEA executable uses the official Node.js resource format:
@@ -277,7 +302,7 @@ The SEA executable uses the official Node.js resource format:
│ Node.js binary │
│ with NODE_SEA_FUSE activated │ ← Sentinel fuse flipped
├──────────────────────────────────┤
-│ NODE_SEA_BLOB resource: │ ← Injected via postject
+│ NODE_SEA_BLOB resource: │ ← Injected via node --build-sea (>=25.5) or postject
│ ┌──────────────────────────┐ │
│ │ main: sea-bootstrap.js │ │ ← Bundled bootstrap + VFS polyfill
│ ├──────────────────────────┤ │
@@ -575,13 +600,14 @@ For users who require code protection with SEA mode:
### Current (April 2026)
-| Dependency | Purpose | Status |
-| --------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `node:sea` API | Archive blob and manifest storage/retrieval in SEA executables | Stable, Node 20+ (pkg requires 22+, aligned with `engines.node`) |
-| `@roberts_lando/vfs` | VFS polyfill — patches `fs`, `fs/promises`, and module loader | Published, Node 22+, maintained by Matteo Collina |
-| `postject` | Injects `NODE_SEA_BLOB` resource into executables | Stable, used by Node.js project |
-| `--experimental-sea-config` | Generates the prep blob consumed by postject | Stable, Node 22+. Used on every target — `--build-sea` is intentionally NOT used because it produces a finished executable and bypasses the prep-blob + postject flow pkg needs for multi-target injection |
-| `mainFormat: "module"` | Native ESM SEA main in sea-config | Not used. Node 25.5+'s embedder `importModuleDynamicallyForEmbedder` callback only resolves builtins ([nodejs/node#62726](https://github.com/nodejs/node/issues/62726)), so a native ESM main cannot import the user entry |
+| Dependency | Purpose | Status |
+| --------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `node:sea` API | Archive blob and manifest storage/retrieval in SEA executables | Stable, Node 20+ (pkg requires 22+, aligned with `engines.node`) |
+| `@roberts_lando/vfs` | VFS polyfill — patches `fs`, `fs/promises`, and module loader | Published, Node 22+, maintained by Matteo Collina |
+| `postject` | Injects `NODE_SEA_BLOB` resource into executables | Stable. Fallback injector when the generator Node is < 25.5. NB: postject's vendored LIEF (0.13) corrupts the dynamic symbol table of PIE/ET_DYN base binaries, breaking native addons — fixed in LIEF 0.16.7 / Node's in-core `--build-sea` |
+| `--experimental-sea-config` | Generates the prep blob (postject fallback path only) | Stable, Node 22+. Used when the generator Node is < 25.5 |
+| `--build-sea` | Generates the blob **and** injects it in-core (current LIEF) | Node 25.5+. Preferred path: reads the base binary from the config `executable` field and writes `output`, so one host-runnable Node injects each target's base directly |
+| `mainFormat: "module"` | Native ESM SEA main in sea-config | Not used. Node 25.5+'s embedder `importModuleDynamicallyForEmbedder` callback only resolves builtins ([nodejs/node#62726](https://github.com/nodejs/node/issues/62726)), so a native ESM main cannot import the user entry |
### Future
diff --git a/lib/config.ts b/lib/config.ts
index 716febe0..2287adc1 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -138,6 +138,25 @@ const FLAG_SPECS: readonly FlagSpec[] = [
default: false,
},
{ cli: 'sea', cfg: 'sea', resolved: 'sea', kind: 'bool', default: false },
+ {
+ // SEA mode: use a specific Node binary as the base for the executable
+ // instead of downloading one from nodejs.org. Lets you embed a custom
+ // build (e.g. one that runs on older glibc, or a differently-configured
+ // runtime). The binary's major version must match the target's.
+ cli: 'sea-node-path',
+ cfg: 'seaNodePath',
+ resolved: 'seaNodePath',
+ kind: 'string',
+ },
+ {
+ // SEA mode: use the Node binary currently running pkg (process.execPath)
+ // as the base. Shorthand for `--sea-node-path "$(command -v node)"`.
+ cli: 'sea-use-local-node',
+ cfg: 'seaUseLocalNode',
+ resolved: 'seaUseLocalNode',
+ kind: 'bool',
+ default: false,
+ },
{
cli: 'compress',
cfg: 'compress',
@@ -485,6 +504,10 @@ export interface ResolvedFlags {
fallbackToSource: boolean;
public: boolean;
sea: boolean;
+ /** SEA mode: path to a base Node binary to embed (overrides the download). */
+ seaNodePath: string | undefined;
+ /** SEA mode: embed the Node binary running pkg (process.execPath) as the base. */
+ seaUseLocalNode: boolean;
publicPackages: string[] | undefined;
noDictionary: string[] | undefined;
bakeOptions: string[] | undefined;
diff --git a/lib/help.ts b/lib/help.ts
index 31f0e613..3886b7b1 100644
--- a/lib/help.ts
+++ b/lib/help.ts
@@ -26,10 +26,13 @@ export default function help() {
--signature enable macOS binary signing (default; use to override signature:false in config)
--no-signature skip macOS binary signing [default: sign]
--sea (Experimental) compile given file using node's SEA feature. Requires node v20.0.0 or higher and only single file is supported
+ --sea-node-path SEA mode: path to a base Node binary to embed instead of downloading one (must match the target's major version)
+ --sea-use-local-node SEA mode: embed the Node binary running pkg (process.execPath) as the base
All build-shaping flags above (compress, fallback-to-source, public, public-packages,
- options, bytecode, native-build, no-dict, debug, signature, sea) can also be set in
- the pkg config file (camelCase keys). CLI flags override config values.
+ options, bytecode, native-build, no-dict, debug, signature, sea, sea-node-path,
+ sea-use-local-node) can also be set in the pkg config file (camelCase keys). CLI flags
+ override config values.
${pc.dim('Examples:')}
diff --git a/lib/index.ts b/lib/index.ts
index 4b224def..e4287430 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -192,6 +192,18 @@ export async function exec(
}
if (flags.sea) {
+ // Base-node override (both forms are mutually exclusive). nodePath embeds a
+ // specific binary; useLocalNode embeds the Node running pkg. Either lets you
+ // ship a SEA built on a custom runtime (e.g. one linked against older glibc).
+ if (flags.seaNodePath && flags.seaUseLocalNode) {
+ throw wasReported(
+ "Specify either '--sea-node-path' or '--sea-use-local-node', not both",
+ );
+ }
+ const seaBase = {
+ nodePath: flags.seaNodePath,
+ useLocalNode: flags.seaUseLocalNode,
+ };
if (inputJson || configJson) {
// Enhanced SEA mode — use walker pipeline.
// seaEnhanced validates the host Node version and minTargetMajor itself.
@@ -202,6 +214,7 @@ export async function exec(
params: { ...params, seaMode: true },
addition: isConfiguration(input) ? input : undefined,
doCompress: flags.compress,
+ ...seaBase,
});
} else {
// Simple SEA mode — plain .js file without package.json.
@@ -216,6 +229,7 @@ export async function exec(
await sea(inputFin, {
targets,
signature: flags.signature,
+ ...seaBase,
});
}
return;
diff --git a/lib/sea.ts b/lib/sea.ts
index a2e976b2..cf369f85 100644
--- a/lib/sea.ts
+++ b/lib/sea.ts
@@ -721,12 +721,13 @@ async function pickBlobGeneratorBinary(
}
/**
- * Generate the SEA prep blob from a sea-config.json file.
+ * Generate the SEA prep blob from a sea-config.json file (the postject
+ * fallback path, used when the generator Node is older than 25.5).
*
- * Uses --experimental-sea-config (not --build-sea): --build-sea produces
- * a finished executable and bypasses the prep-blob + postject flow that
- * pkg relies on for multi-target support and for injecting custom
- * bootstraps into downloaded node binaries.
+ * Uses --experimental-sea-config, which only produces the prep blob; the
+ * separate postject inject step then bakes it into each target. Newer Node
+ * (>= 25.5) skips this and uses the in-core `--build-sea` instead -- see
+ * {@link injectAllTargets}.
*/
async function generateSeaBlob(
seaConfigFilePath: string,
@@ -739,6 +740,99 @@ async function generateSeaBlob(
]);
}
+/**
+ * Whether a Node version string (`vX.Y.Z` or `X.Y.Z`) supports the in-core
+ * `node --build-sea` flag, which landed in Node v25.5.0. `--build-sea` performs
+ * the blob generation AND the binary injection in one step using Node's bundled
+ * (current) LIEF -- so it bypasses the external `postject`, whose older vendored
+ * LIEF corrupts the dynamic symbol table of PIE (ET_DYN) binaries and breaks
+ * native-addon symbol resolution in the resulting SEA.
+ */
+export function supportsBuildSea(version: string): boolean {
+ const m = /v?(\d+)\.(\d+)\./.exec(version);
+ if (!m) return false;
+ const major = Number(m[1]);
+ const minor = Number(m[2]);
+ return major > 25 || (major === 25 && minor >= 5);
+}
+
+/**
+ * Inject the SEA payload into every target binary.
+ *
+ * When the host-runnable generator Node supports `--build-sea` (>= 25.5) we use
+ * it: Node reads the base binary from the config's `executable` field, generates
+ * the blob, injects it with its bundled LIEF, and writes the finished executable
+ * to `output` -- no prep-blob + postject round-trip. `assertSingleTargetMajor`
+ * guarantees all targets share the generator's major, so the generated blob is
+ * compatible with every target.
+ *
+ * Otherwise we fall back to the classic path: generate one prep blob and inject
+ * it into each target copy with postject.
+ */
+async function injectAllTargets(args: {
+ targets: (NodeTarget & Partial)[];
+ nodePaths: string[];
+ generatorBinary: string;
+ signature?: boolean;
+ seaConfig: Record;
+ seaConfigFilePath: string;
+ blobPath: string;
+ tmpDir: string;
+}): Promise {
+ const {
+ targets,
+ nodePaths,
+ generatorBinary,
+ signature,
+ seaConfig,
+ seaConfigFilePath,
+ blobPath,
+ tmpDir,
+ } = args;
+
+ const generatorVersion = (
+ await execFileAsync(generatorBinary, ['--version'])
+ ).stdout.trim();
+
+ if (supportsBuildSea(generatorVersion)) {
+ log.info(
+ `Injecting the blob with "node ${generatorVersion} --build-sea" (in-core LIEF; postject not used)...`,
+ );
+ await Promise.all(
+ nodePaths.map(async (nodePath, i) => {
+ const target = targets[i];
+ const outPath = resolve(process.cwd(), target.output!);
+ await mkdir(dirname(outPath), { recursive: true });
+ // Per-target config: same payload, but point `executable` at this
+ // target's base binary and `output` at the finished executable.
+ const cfgPath = join(tmpDir, `sea-config-build-${i}.json`);
+ await writeFile(
+ cfgPath,
+ JSON.stringify({
+ ...seaConfig,
+ executable: nodePath,
+ output: outPath,
+ }),
+ );
+ await execFileAsync(generatorBinary, ['--build-sea', cfgPath]);
+ await signMacOSIfNeeded(target.output!, target, signature, true);
+ }),
+ );
+ return;
+ }
+
+ await generateSeaBlob(seaConfigFilePath, generatorBinary);
+ // Read the blob once and share the buffer across all targets.
+ const blobData = await readFile(blobPath);
+ await Promise.all(
+ nodePaths.map(async (nodePath, i) => {
+ const target = targets[i];
+ await bake(nodePath, target, blobData);
+ await signMacOSIfNeeded(target.output!, target, signature, true);
+ }),
+ );
+}
+
/** Create NodeJS executable using the enhanced SEA pipeline (walker + refiner + assets) */
export async function seaEnhanced(
entryPoint: string,
@@ -859,23 +953,21 @@ export async function seaEnhanced(
log.info('Creating sea-config.json file...');
await writeFile(seaConfigFilePath, JSON.stringify(seaConfig));
- await generateSeaBlob(
- seaConfigFilePath,
- await pickBlobGeneratorBinary(opts.targets, nodePaths, opts),
- );
-
- // Read the blob once and share the buffer across all targets — avoids
- // N redundant disk reads and N peak buffer copies on multi-target builds.
- const blobData = await readFile(blobPath);
-
- // Bake blob into each target executable
- await Promise.all(
- nodePaths.map(async (nodePath, i) => {
- const target = opts.targets[i];
- await bake(nodePath, target, blobData);
- await signMacOSIfNeeded(target.output!, target, opts.signature, true);
- }),
+ const generatorBinary = await pickBlobGeneratorBinary(
+ opts.targets,
+ nodePaths,
+ opts,
);
+ await injectAllTargets({
+ targets: opts.targets,
+ nodePaths,
+ generatorBinary,
+ signature: opts.signature,
+ seaConfig,
+ seaConfigFilePath,
+ blobPath,
+ tmpDir,
+ });
});
}
@@ -910,19 +1002,20 @@ export default async function sea(entryPoint: string, opts: SeaOptions) {
log.info('Creating sea-config.json file...');
await writeFile(seaConfigFilePath, JSON.stringify(seaConfig));
- await generateSeaBlob(
- seaConfigFilePath,
- await pickBlobGeneratorBinary(opts.targets, nodePaths, opts),
- );
-
- const blobData = await readFile(blobPath);
-
- await Promise.all(
- nodePaths.map(async (nodePath, i) => {
- const target = opts.targets[i];
- await bake(nodePath, target, blobData);
- await signMacOSIfNeeded(target.output!, target, opts.signature, true);
- }),
+ const generatorBinary = await pickBlobGeneratorBinary(
+ opts.targets,
+ nodePaths,
+ opts,
);
+ await injectAllTargets({
+ targets: opts.targets,
+ nodePaths,
+ generatorBinary,
+ signature: opts.signature,
+ seaConfig,
+ seaConfigFilePath,
+ blobPath,
+ tmpDir,
+ });
});
}
diff --git a/lib/types.ts b/lib/types.ts
index 8f1121f6..c1fcf798 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -66,6 +66,10 @@ export interface PkgOptions {
debug?: boolean;
signature?: boolean;
sea?: boolean;
+ /** SEA mode: path to a base Node binary to embed (overrides the download). */
+ seaNodePath?: string;
+ /** SEA mode: embed the Node binary running pkg (process.execPath) as the base. */
+ seaUseLocalNode?: boolean;
}
export interface PackageJson {
@@ -200,4 +204,12 @@ export interface PkgExecOptions {
noDictionary?: string[];
/** Sign macOS binaries when applicable. Default `true`. */
signature?: boolean;
+ /**
+ * SEA mode: path to a base Node binary to embed instead of downloading one
+ * from nodejs.org. Use to embed a custom build (e.g. one linked against an
+ * older glibc). Its major version must match the target's.
+ */
+ seaNodePath?: string;
+ /** SEA mode: embed the Node binary running pkg (`process.execPath`) as the base. */
+ seaUseLocalNode?: boolean;
}
diff --git a/test/test-95-sea-local-node/index.js b/test/test-95-sea-local-node/index.js
new file mode 100644
index 00000000..d600887a
--- /dev/null
+++ b/test/test-95-sea-local-node/index.js
@@ -0,0 +1,6 @@
+'use strict';
+
+// Packaged into a SEA with `--sea-use-local-node`, so the embedded runtime is
+// the same Node that ran pkg (rather than a downloaded one). Output is asserted
+// by main.js. process.pkg confirms the enhanced SEA bootstrap is active.
+console.log('local-node SEA OK:' + (process.pkg != null));
diff --git a/test/test-95-sea-local-node/main.js b/test/test-95-sea-local-node/main.js
new file mode 100644
index 00000000..fee76f57
--- /dev/null
+++ b/test/test-95-sea-local-node/main.js
@@ -0,0 +1,29 @@
+#!/usr/bin/env node
+
+'use strict';
+
+const assert = require('assert');
+const utils = require('../utils.js');
+
+// Enhanced SEA requires Node.js >= 22
+if (utils.getNodeMajorVersion() < 22) {
+ return;
+}
+
+assert(__dirname === process.cwd());
+
+const input = './package.json';
+const testName = 'test-95-sea-local-node';
+
+const newcomers = utils.seaHostOutputs(testName);
+
+const before = utils.filesBefore(newcomers);
+
+// `--sea-use-local-node` embeds the Node binary running pkg as the SEA base
+// instead of downloading one. Exercises the base-node override end to end (and,
+// on Node >= 25.5 hosts, the in-core `--build-sea` injection path).
+utils.runSeaHostOnly(input, testName, ['--sea-use-local-node']);
+
+utils.assertSeaOutput(testName, 'local-node SEA OK:true\n');
+
+utils.filesAfter(before, newcomers, { tolerateWindowsEbusy: true });
diff --git a/test/test-95-sea-local-node/package.json b/test/test-95-sea-local-node/package.json
new file mode 100644
index 00000000..4d8cec14
--- /dev/null
+++ b/test/test-95-sea-local-node/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "test-95-sea-local-node",
+ "version": "1.0.0",
+ "main": "index.js",
+ "bin": "index.js"
+}
diff --git a/test/test.js b/test/test.js
index 88c701af..95532c79 100644
--- a/test/test.js
+++ b/test/test.js
@@ -84,6 +84,7 @@ const npmTests = [
'test-91-sea-esm-entry',
'test-92-sea-tla',
'test-94-sea-esm-import-meta',
+ 'test-95-sea-local-node',
];
if (testFilter) {
diff --git a/test/unit/config-parse.test.ts b/test/unit/config-parse.test.ts
index 49b02293..09e19b8c 100644
--- a/test/unit/config-parse.test.ts
+++ b/test/unit/config-parse.test.ts
@@ -165,6 +165,24 @@ describe('parseInput — CLI argv', () => {
it('--options "" preserves the explicit empty signal', () => {
assert.equal(parseInput(['--options', '', 'a.js']).flags.options, '');
});
+
+ it('string flag --sea-node-path (kebab key preserved)', () => {
+ assert.equal(
+ parseInput(['--sea-node-path', '/opt/node/bin/node', 'a.js']).flags[
+ 'sea-node-path'
+ ],
+ '/opt/node/bin/node',
+ );
+ });
+
+ it('bool flag --sea-use-local-node (kebab key preserved)', () => {
+ assert.equal(
+ parseInput(['--sea-use-local-node', 'a.js']).flags[
+ 'sea-use-local-node'
+ ],
+ true,
+ );
+ });
});
describe('negation', () => {
@@ -410,6 +428,25 @@ describe('resolveFlags — CLI > config > default', () => {
assert.equal(f.bytecode, false);
});
+ it('SEA base-node override — defaults, config, CLI precedence', () => {
+ const def = resolveFlags({}, {});
+ assert.equal(def.seaNodePath, undefined);
+ assert.equal(def.seaUseLocalNode, false);
+
+ // config keys (cfg name) resolve when CLI is absent
+ const cfg = resolveFlags({}, { seaNodePath: '/n', seaUseLocalNode: true });
+ assert.equal(cfg.seaNodePath, '/n');
+ assert.equal(cfg.seaUseLocalNode, true);
+
+ // CLI (kebab cli name) overrides config
+ const cli = resolveFlags(
+ { 'sea-node-path': '/cli', 'sea-use-local-node': true },
+ { seaNodePath: '/cfg' },
+ );
+ assert.equal(cli.seaNodePath, '/cli');
+ assert.equal(cli.seaUseLocalNode, true);
+ });
+
describe('list handling', () => {
it('CLI "" clears the configured list (empty wins)', () => {
const f = resolveFlags({ options: '' }, { options: ['expose-gc'] });
diff --git a/test/unit/sea-build-sea.test.ts b/test/unit/sea-build-sea.test.ts
new file mode 100644
index 00000000..e1b9c8cc
--- /dev/null
+++ b/test/unit/sea-build-sea.test.ts
@@ -0,0 +1,35 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+
+import { supportsBuildSea } from '../../lib/sea';
+
+// `node --build-sea` (in-core SEA blob injection via Node's bundled, current
+// LIEF) landed in Node v25.5.0. pkg prefers it over the external postject when
+// the generator Node is new enough — postject's old vendored LIEF corrupts the
+// dynamic symbol table of PIE binaries, breaking native addons in the SEA. This
+// suite pins the version boundary; the full --build-sea path is covered by the
+// SEA e2e tests when run on a >= 25.5 host.
+describe('supportsBuildSea', () => {
+ it('is false below 25.5', () => {
+ assert.equal(supportsBuildSea('v24.16.0'), false);
+ assert.equal(supportsBuildSea('v25.0.0'), false);
+ assert.equal(supportsBuildSea('v25.4.9'), false);
+ });
+
+ it('is true at the 25.5.0 boundary and above', () => {
+ assert.equal(supportsBuildSea('v25.5.0'), true);
+ assert.equal(supportsBuildSea('v25.6.0'), true);
+ assert.equal(supportsBuildSea('v26.3.0'), true);
+ assert.equal(supportsBuildSea('v30.0.0'), true);
+ });
+
+ it('accepts versions with or without a leading "v"', () => {
+ assert.equal(supportsBuildSea('25.5.0'), true);
+ assert.equal(supportsBuildSea('24.16.0'), false);
+ });
+
+ it('is false for unparseable input', () => {
+ assert.equal(supportsBuildSea(''), false);
+ assert.equal(supportsBuildSea('not-a-version'), false);
+ });
+});