@needle-tools/gltf-build-pipeline 3.0.0-alpha → 3.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +75 -39
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1190,7 +1190,7 @@ function registerExtensions(node) {
|
|
|
1190
1190
|
import { createTransform } from "@gltf-transform/functions";
|
|
1191
1191
|
|
|
1192
1192
|
// src/utils/version.gen.ts
|
|
1193
|
-
var version = "3.0.0-alpha";
|
|
1193
|
+
var version = "3.0.0-alpha.2";
|
|
1194
1194
|
|
|
1195
1195
|
// src/utils/version.ts
|
|
1196
1196
|
function getVersion(_marklocal = !0) {
|
|
@@ -3805,7 +3805,7 @@ function resolveConfig(filename, content, logger) {
|
|
|
3805
3805
|
}
|
|
3806
3806
|
|
|
3807
3807
|
// src/cli/index.ts
|
|
3808
|
-
import { existsSync as
|
|
3808
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync7, writeFileSync as writeFileSync7 } from "fs";
|
|
3809
3809
|
import path7, { basename as basename4, dirname as dirname8, resolve as resolve3 } from "path";
|
|
3810
3810
|
|
|
3811
3811
|
// src/constants.ts
|
|
@@ -3901,7 +3901,7 @@ function trackPipelineEnd(info) {
|
|
|
3901
3901
|
}
|
|
3902
3902
|
|
|
3903
3903
|
// src/auth/verify.ts
|
|
3904
|
-
import { jwtVerify, importJWK } from "jose";
|
|
3904
|
+
import { jwtVerify, importJWK, decodeProtectedHeader } from "jose";
|
|
3905
3905
|
|
|
3906
3906
|
// src/auth/keys.ts
|
|
3907
3907
|
var PROD_KEYS = [
|
|
@@ -3922,56 +3922,92 @@ var PROD_KEYS = [
|
|
|
3922
3922
|
var trusted = [...PROD_KEYS], TRUSTED_KEYS = trusted;
|
|
3923
3923
|
|
|
3924
3924
|
// src/auth/verify.ts
|
|
3925
|
+
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
3926
|
+
import { join as join8 } from "path";
|
|
3927
|
+
import { homedir } from "os";
|
|
3925
3928
|
var AuthError = class extends Error {
|
|
3926
3929
|
constructor(message) {
|
|
3927
3930
|
super(message), this.name = "AuthError";
|
|
3928
3931
|
}
|
|
3929
|
-
};
|
|
3932
|
+
}, LOCAL_FETCH_TIMEOUT_MS = 2e3;
|
|
3933
|
+
function getLocalServerToken() {
|
|
3934
|
+
try {
|
|
3935
|
+
let file = join8(homedir(), ".needle", "local-server-token");
|
|
3936
|
+
if (!existsSync13(file)) return null;
|
|
3937
|
+
let data = JSON.parse(readFileSync10(file, "utf-8"));
|
|
3938
|
+
return typeof data.token == "string" ? data.token : null;
|
|
3939
|
+
} catch {
|
|
3940
|
+
return null;
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
async function tryFetchLocalToken(org) {
|
|
3944
|
+
try {
|
|
3945
|
+
let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", url = new URL(baseUrl);
|
|
3946
|
+
org && url.searchParams.set("org", org);
|
|
3947
|
+
let headers = {}, serverToken = getLocalServerToken();
|
|
3948
|
+
serverToken && (headers.Authorization = `Bearer ${serverToken}`);
|
|
3949
|
+
let res = await fetch(url, {
|
|
3950
|
+
signal: AbortSignal.timeout(LOCAL_FETCH_TIMEOUT_MS),
|
|
3951
|
+
headers
|
|
3952
|
+
});
|
|
3953
|
+
if (!res.ok) return null;
|
|
3954
|
+
let jwt = (await res.json()).needle_license_jwt;
|
|
3955
|
+
return typeof jwt == "string" && jwt.length > 0 ? jwt : null;
|
|
3956
|
+
} catch {
|
|
3957
|
+
return null;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
async function verifyWithKey(token, key) {
|
|
3961
|
+
if (!key.alg) throw new AuthError(`trusted key '${key.kid ?? "?"}' is missing 'alg'`);
|
|
3962
|
+
let keyObj = await importJWK(key, key.alg);
|
|
3963
|
+
await jwtVerify(token, keyObj);
|
|
3964
|
+
}
|
|
3930
3965
|
async function verifyAuthToken(token) {
|
|
3931
3966
|
if (!token)
|
|
3932
3967
|
throw new AuthError("no token provided");
|
|
3933
3968
|
if (token.startsWith("nc_"))
|
|
3934
3969
|
throw new AuthError("NEEDLE_CLOUD_TOKEN is a needle-cloud access token (nc_*), not a JWT.");
|
|
3935
3970
|
try {
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
throw new AuthError("token header missing 'kid'");
|
|
3971
|
+
let header = decodeProtectedHeader(token);
|
|
3972
|
+
if (header.kid) {
|
|
3939
3973
|
let match = TRUSTED_KEYS.find((k) => k.kid === header.kid);
|
|
3940
3974
|
if (!match)
|
|
3941
3975
|
throw new AuthError(`token signed by unknown key '${header.kid}'`);
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3976
|
+
await verifyWithKey(token, match);
|
|
3977
|
+
} else {
|
|
3978
|
+
let lastError = null;
|
|
3979
|
+
for (let key of TRUSTED_KEYS)
|
|
3980
|
+
try {
|
|
3981
|
+
await verifyWithKey(token, key);
|
|
3982
|
+
return;
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
lastError = err;
|
|
3985
|
+
}
|
|
3986
|
+
if (lastError instanceof AuthError) throw lastError;
|
|
3987
|
+
let message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
3988
|
+
throw new AuthError(message || "no trusted key could verify this token");
|
|
3989
|
+
}
|
|
3951
3990
|
} catch (err) {
|
|
3952
3991
|
if (err instanceof AuthError) throw err;
|
|
3953
|
-
if (err && typeof err == "object" && "code" in err)
|
|
3954
|
-
|
|
3955
|
-
if (code === "ERR_JWT_EXPIRED")
|
|
3956
|
-
throw new AuthError("token expired");
|
|
3957
|
-
if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED" && "claim" in err && err.claim === "exp")
|
|
3958
|
-
throw new AuthError("token missing 'exp' claim");
|
|
3959
|
-
}
|
|
3992
|
+
if (err && typeof err == "object" && "code" in err && err.code === "ERR_JWT_EXPIRED")
|
|
3993
|
+
throw new AuthError("token expired");
|
|
3960
3994
|
let message = err instanceof Error ? err.message : String(err);
|
|
3961
3995
|
throw new AuthError(message);
|
|
3962
3996
|
}
|
|
3963
3997
|
}
|
|
3964
|
-
async function requireAuth(cliToken) {
|
|
3998
|
+
async function requireAuth(cliToken, org) {
|
|
3965
3999
|
let token = typeof cliToken == "string" && cliToken.length > 0 ? cliToken : process.env.NEEDLE_CLOUD_TOKEN;
|
|
3966
|
-
if (!token)
|
|
3967
|
-
throw new AuthError(
|
|
4000
|
+
if ((!token || typeof token == "string" && token.length === 0) && (token = await tryFetchLocalToken(org ?? process.env.NEEDLE_CLOUD_ORG)), !token)
|
|
4001
|
+
throw new AuthError(
|
|
4002
|
+
"No valid auth token found. Provide one via --auth-token, NEEDLE_CLOUD_TOKEN env, or log in to the needle-cloud CLI (npx needle-cloud@version-2 start --restart). If you are logged in but still see this error, try updating: npx needle-cloud@version-2 start --restart"
|
|
4003
|
+
);
|
|
3968
4004
|
await verifyAuthToken(token);
|
|
3969
4005
|
}
|
|
3970
4006
|
|
|
3971
4007
|
// src/cli/index.ts
|
|
3972
|
-
async function gateAuth(authToken, logger) {
|
|
4008
|
+
async function gateAuth(authToken, org, logger) {
|
|
3973
4009
|
try {
|
|
3974
|
-
await requireAuth(authToken);
|
|
4010
|
+
await requireAuth(authToken, typeof org == "string" ? org : void 0);
|
|
3975
4011
|
} catch (err) {
|
|
3976
4012
|
let message = err instanceof AuthError || err instanceof Error ? err.message : String(err);
|
|
3977
4013
|
logger.error(`[Needle Build Pipeline] Auth failed: ${message}`), process.exit(ERROR_CODES.AUTH_FAILED);
|
|
@@ -3985,8 +4021,8 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
|
|
|
3985
4021
|
`).action(async ({ logger }) => {
|
|
3986
4022
|
logger.info("Clearing caches"), clearCache(logger);
|
|
3987
4023
|
}).command("stats", "Calculate glTF stats").help(`
|
|
3988
|
-
`).argument("<input>", "Path to a glTF, GLB or VRM file OR a directory").argument("<output>", "Output path for the stats", { default: null, validator: program.STRING }).option("--verbose", "Enable verbose output", { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).action(async ({ logger, args, options }) => {
|
|
3989
|
-
await gateAuth(options.authToken, logger), logger.level = options.verbose ? "debug" : "info", await ensureHashReady(), logger.info('Calculating stats for "' + args.input + '"');
|
|
4024
|
+
`).argument("<input>", "Path to a glTF, GLB or VRM file OR a directory").argument("<output>", "Output path for the stats", { default: null, validator: program.STRING }).option("--verbose", "Enable verbose output", { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).option("--org <org>", "Organization ID for local license-server fallback (or set NEEDLE_CLOUD_ORG).", { validator: program.STRING }).action(async ({ logger, args, options }) => {
|
|
4025
|
+
await gateAuth(options.authToken, options.org, logger), logger.level = options.verbose ? "debug" : "info", await ensureHashReady(), logger.info('Calculating stats for "' + args.input + '"');
|
|
3990
4026
|
let input = args.input.toString(), filestats = new Array();
|
|
3991
4027
|
await foreachGLTF(input, async (file) => {
|
|
3992
4028
|
let stats = await calculateStats(file, { verbose: !!options.verbose });
|
|
@@ -3998,8 +4034,8 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
|
|
|
3998
4034
|
This will produce multiple versions of the input file, each with a different level of optimization.
|
|
3999
4035
|
Each version will be compressed and written to the output directory.
|
|
4000
4036
|
`).argument("<input>", "The input glTF, GLB or VRM file that should be compressed OR a directory that contains files to be compressed. Multiple files can be separated by commas.", { validator: program.STRING }).argument("<output>", "The output file or directory. It can be an absolute path or a relative path to the input file. If not provided, the input file will be overwritten.", { default: null, validator: program.STRING }).option("--compress", "When enabled files will be compressed", { validator: program.BOOLEAN, default: !0 }).option("--progressive", "When enabled files will processed to be progressively loaded", { validator: program.BOOLEAN, default: !0 }).option("--usecase <usecase>", `The usecase for the compression. This will set the compression settings. Possible options: [${UsecaseOptions.join(", ")}]`, { validator: program.STRING }).option("--config <config>", 'The path to a JSON file containing the compression settings. This can also be a needle.config.json file with a "compression" object or usecase string.', { validator: program.STRING }).option("--cache", `Use "--cache False" to disable caching.
|
|
4001
|
-
When caching is enabled then previously processed files will be detected and loaded from disc cache instead of being processed again if settings or the file contents have not changed.`, { validator: program.BOOLEAN, default: !0 }).option("--debug", 'Enable for more verbose outout with "--debug True"', { validator: program.BOOLEAN, default: !1 }).option("--verbose", 'Enable for more verbose outout with "--verbose True"', { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).action(async ({ logger, args, options }) => {
|
|
4002
|
-
if (await gateAuth(options.authToken, logger), logger.level = options.debug || options.verbose ? "debug" : "info", options.compress === !1 && options.progressive === !1) {
|
|
4037
|
+
When caching is enabled then previously processed files will be detected and loaded from disc cache instead of being processed again if settings or the file contents have not changed.`, { validator: program.BOOLEAN, default: !0 }).option("--debug", 'Enable for more verbose outout with "--debug True"', { validator: program.BOOLEAN, default: !1 }).option("--verbose", 'Enable for more verbose outout with "--verbose True"', { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).option("--org <org>", "Organization ID for local license-server fallback (or set NEEDLE_CLOUD_ORG).", { validator: program.STRING }).action(async ({ logger, args, options }) => {
|
|
4038
|
+
if (await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", options.compress === !1 && options.progressive === !1) {
|
|
4003
4039
|
logger.error(`[Needle Build Pipeline] v${getVersion()} You need to enable at least one of the options: --compress or --progressive`);
|
|
4004
4040
|
return;
|
|
4005
4041
|
}
|
|
@@ -4027,7 +4063,7 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4027
4063
|
}
|
|
4028
4064
|
let set = /* @__PURE__ */ new Set(), useCache = options.cache !== !1, debug = options.debug === !0, verbose = options.verbose === !0;
|
|
4029
4065
|
await foreachGLTF(inputPaths, async (file) => {
|
|
4030
|
-
if (
|
|
4066
|
+
if (existsSync14(file)) {
|
|
4031
4067
|
if (file != input && options.progressive === !0 && isLOD(file)) {
|
|
4032
4068
|
logger.debug(`\u2192 Ignore existing LOD file at ${file}`);
|
|
4033
4069
|
return;
|
|
@@ -4043,7 +4079,7 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4043
4079
|
config
|
|
4044
4080
|
}, file) : void 0;
|
|
4045
4081
|
if (jobKey) {
|
|
4046
|
-
let outputDir = output ?
|
|
4082
|
+
let outputDir = output ? existsSync14(output) && isDirectory(output) ? output : dirname8(output) : dirname8(file), restored = tryRestoreJobResult(jobKey, outputDir, logger);
|
|
4047
4083
|
if (restored) {
|
|
4048
4084
|
let inputFileSize = statSync7(file).size / 1024 / 1024;
|
|
4049
4085
|
for (let f of restored)
|
|
@@ -4116,10 +4152,10 @@ When caching is enabled then previously processed files will be detected and loa
|
|
|
4116
4152
|
producing <name>.pmrem.ktx2. This is the same processing the Needle build pipeline
|
|
4117
4153
|
applies to EXR textures inside GLB/GLTF files, exposed as a standalone command for
|
|
4118
4154
|
raw .exr / .hdr inputs.
|
|
4119
|
-
`).argument("<input>", "Path to a .exr/.hdr file OR a directory containing .exr/.hdr files (recursive)", { validator: program.STRING }).argument("<output>", "Output file (when input is a single .exr/.hdr) or output directory. Defaults to writing next to the input file.", { default: null, validator: program.STRING }).option("--cache", 'Use "--cache False" to disable caching.', { validator: program.BOOLEAN, default: !0 }).option("--debug", 'Enable verbose output with "--debug True"', { validator: program.BOOLEAN, default: !1 }).option("--verbose", 'Enable verbose output with "--verbose True"', { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).hide().action(async ({ logger, args, options }) => {
|
|
4120
|
-
await gateAuth(options.authToken, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
|
|
4155
|
+
`).argument("<input>", "Path to a .exr/.hdr file OR a directory containing .exr/.hdr files (recursive)", { validator: program.STRING }).argument("<output>", "Output file (when input is a single .exr/.hdr) or output directory. Defaults to writing next to the input file.", { default: null, validator: program.STRING }).option("--cache", 'Use "--cache False" to disable caching.', { validator: program.BOOLEAN, default: !0 }).option("--debug", 'Enable verbose output with "--debug True"', { validator: program.BOOLEAN, default: !1 }).option("--verbose", 'Enable verbose output with "--verbose True"', { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).option("--org <org>", "Organization ID for local license-server fallback (or set NEEDLE_CLOUD_ORG).", { validator: program.STRING }).hide().action(async ({ logger, args, options }) => {
|
|
4156
|
+
await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
|
|
4121
4157
|
let input = args.input.toString();
|
|
4122
|
-
|
|
4158
|
+
existsSync14(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
|
|
4123
4159
|
let inputFiles = [], inputIsDirectory = isDirectory(input);
|
|
4124
4160
|
if (inputIsDirectory ? collectPmremInputFiles(input, inputFiles) : isPmremInputFile(input) ? inputFiles.push(input) : (logger.error(`Input is not an .exr/.hdr file or directory: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS)), inputFiles.length === 0) {
|
|
4125
4161
|
logger.warn(`No .exr/.hdr files found in ${input}`);
|
|
@@ -4138,7 +4174,7 @@ raw .exr / .hdr inputs.
|
|
|
4138
4174
|
for (let inPath of inputFiles) {
|
|
4139
4175
|
let outPath = resolvePmremOutputPath(inPath, input, inputIsDirectory, outputArg);
|
|
4140
4176
|
logger.info(`\u2192 pmrem ${inPath} \u2192 ${outPath}`);
|
|
4141
|
-
let srcBytes =
|
|
4177
|
+
let srcBytes = readFileSync11(inPath), bytes = new Uint8Array(srcBytes.buffer, srcBytes.byteOffset, srcBytes.byteLength), ktx2Bytes = await (inPath.toLowerCase().endsWith(".hdr") ? convertHdrBytesToPmremKtx2 : convertExrBytesToPmremKtx2)(
|
|
4142
4178
|
runtime,
|
|
4143
4179
|
bytes,
|
|
4144
4180
|
{ useCache, logLabel: `pmrem ${basename4(inPath)}` }
|
|
@@ -4169,7 +4205,7 @@ function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
|
|
|
4169
4205
|
let rel = path7.relative(input, srcPath), relDir = dirname8(rel), outDir = relDir === "." ? outputArg : path7.join(outputArg, relDir);
|
|
4170
4206
|
return ensureIsDirectory(outDir), path7.join(outDir, outName);
|
|
4171
4207
|
}
|
|
4172
|
-
return
|
|
4208
|
+
return existsSync14(outputArg) && isDirectory(outputArg) ? path7.join(outputArg, outName) : outputArg.toLowerCase().endsWith(".ktx2") ? (ensureIsDirectory(dirname8(outputArg)), outputArg) : (ensureIsDirectory(outputArg), path7.join(outputArg, outName));
|
|
4173
4209
|
}
|
|
4174
4210
|
program.run().catch((err) => {
|
|
4175
4211
|
if (!(err.message.includes("Unknown command") && !err.message.includes("Did you mean")))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-build-pipeline",
|
|
3
|
-
"version": "3.0.0-alpha",
|
|
3
|
+
"version": "3.0.0-alpha.2",
|
|
4
4
|
"description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|