@needle-tools/gltf-build-pipeline 3.0.0-alpha → 3.0.0-alpha.1

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +53 -33
  2. 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.1";
1194
1194
 
1195
1195
  // src/utils/version.ts
1196
1196
  function getVersion(_marklocal = !0) {
@@ -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 = [
@@ -3926,52 +3926,72 @@ var AuthError = class extends Error {
3926
3926
  constructor(message) {
3927
3927
  super(message), this.name = "AuthError";
3928
3928
  }
3929
- };
3929
+ }, LOCAL_FETCH_TIMEOUT_MS = 2e3;
3930
+ async function tryFetchLocalToken(org) {
3931
+ try {
3932
+ let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", url = new URL(baseUrl);
3933
+ org && url.searchParams.set("org", org);
3934
+ let res = await fetch(url, {
3935
+ signal: AbortSignal.timeout(LOCAL_FETCH_TIMEOUT_MS)
3936
+ });
3937
+ if (!res.ok) return null;
3938
+ let jwt = (await res.json()).needle_license_jwt;
3939
+ return typeof jwt == "string" && jwt.length > 0 ? jwt : null;
3940
+ } catch {
3941
+ return null;
3942
+ }
3943
+ }
3944
+ async function verifyWithKey(token, key) {
3945
+ if (!key.alg) throw new AuthError(`trusted key '${key.kid ?? "?"}' is missing 'alg'`);
3946
+ let keyObj = await importJWK(key, key.alg);
3947
+ await jwtVerify(token, keyObj);
3948
+ }
3930
3949
  async function verifyAuthToken(token) {
3931
3950
  if (!token)
3932
3951
  throw new AuthError("no token provided");
3933
3952
  if (token.startsWith("nc_"))
3934
3953
  throw new AuthError("NEEDLE_CLOUD_TOKEN is a needle-cloud access token (nc_*), not a JWT.");
3935
3954
  try {
3936
- await jwtVerify(token, async (header) => {
3937
- if (!header.kid)
3938
- throw new AuthError("token header missing 'kid'");
3955
+ let header = decodeProtectedHeader(token);
3956
+ if (header.kid) {
3939
3957
  let match = TRUSTED_KEYS.find((k) => k.kid === header.kid);
3940
3958
  if (!match)
3941
3959
  throw new AuthError(`token signed by unknown key '${header.kid}'`);
3942
- if (!match.alg)
3943
- throw new AuthError(`trusted key '${header.kid}' is missing 'alg'`);
3944
- return importJWK(match, match.alg);
3945
- }, {
3946
- // jose checks `exp` automatically when present; requiredClaims forces
3947
- // the issuer to include it. A token without `exp` would otherwise
3948
- // verify as never-expiring.
3949
- requiredClaims: ["exp"]
3950
- });
3960
+ await verifyWithKey(token, match);
3961
+ } else {
3962
+ let lastError = null;
3963
+ for (let key of TRUSTED_KEYS)
3964
+ try {
3965
+ await verifyWithKey(token, key);
3966
+ return;
3967
+ } catch (err) {
3968
+ lastError = err;
3969
+ }
3970
+ if (lastError instanceof AuthError) throw lastError;
3971
+ let message = lastError instanceof Error ? lastError.message : String(lastError);
3972
+ throw new AuthError(message || "no trusted key could verify this token");
3973
+ }
3951
3974
  } catch (err) {
3952
3975
  if (err instanceof AuthError) throw err;
3953
- if (err && typeof err == "object" && "code" in err) {
3954
- let code = err.code;
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
- }
3976
+ if (err && typeof err == "object" && "code" in err && err.code === "ERR_JWT_EXPIRED")
3977
+ throw new AuthError("token expired");
3960
3978
  let message = err instanceof Error ? err.message : String(err);
3961
3979
  throw new AuthError(message);
3962
3980
  }
3963
3981
  }
3964
- async function requireAuth(cliToken) {
3982
+ async function requireAuth(cliToken, org) {
3965
3983
  let token = typeof cliToken == "string" && cliToken.length > 0 ? cliToken : process.env.NEEDLE_CLOUD_TOKEN;
3966
- if (!token)
3967
- throw new AuthError("pass --auth-token or set NEEDLE_CLOUD_TOKEN");
3984
+ if ((!token || typeof token == "string" && token.length === 0) && (token = await tryFetchLocalToken(org ?? process.env.NEEDLE_CLOUD_ORG)), !token)
3985
+ throw new AuthError(
3986
+ "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-1 start --restart). If you are logged in but still see this error, try updating: npx needle-cloud@version-1 start --restart"
3987
+ );
3968
3988
  await verifyAuthToken(token);
3969
3989
  }
3970
3990
 
3971
3991
  // src/cli/index.ts
3972
- async function gateAuth(authToken, logger) {
3992
+ async function gateAuth(authToken, org, logger) {
3973
3993
  try {
3974
- await requireAuth(authToken);
3994
+ await requireAuth(authToken, typeof org == "string" ? org : void 0);
3975
3995
  } catch (err) {
3976
3996
  let message = err instanceof AuthError || err instanceof Error ? err.message : String(err);
3977
3997
  logger.error(`[Needle Build Pipeline] Auth failed: ${message}`), process.exit(ERROR_CODES.AUTH_FAILED);
@@ -3985,8 +4005,8 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
3985
4005
  `).action(async ({ logger }) => {
3986
4006
  logger.info("Clearing caches"), clearCache(logger);
3987
4007
  }).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 + '"');
4008
+ `).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 }) => {
4009
+ await gateAuth(options.authToken, options.org, logger), logger.level = options.verbose ? "debug" : "info", await ensureHashReady(), logger.info('Calculating stats for "' + args.input + '"');
3990
4010
  let input = args.input.toString(), filestats = new Array();
3991
4011
  await foreachGLTF(input, async (file) => {
3992
4012
  let stats = await calculateStats(file, { verbose: !!options.verbose });
@@ -3998,8 +4018,8 @@ Caches are limited to ${cacheSizeLimit} MB disc space by default.
3998
4018
  This will produce multiple versions of the input file, each with a different level of optimization.
3999
4019
  Each version will be compressed and written to the output directory.
4000
4020
  `).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) {
4021
+ 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 }) => {
4022
+ if (await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", options.compress === !1 && options.progressive === !1) {
4003
4023
  logger.error(`[Needle Build Pipeline] v${getVersion()} You need to enable at least one of the options: --compress or --progressive`);
4004
4024
  return;
4005
4025
  }
@@ -4116,8 +4136,8 @@ When caching is enabled then previously processed files will be detected and loa
4116
4136
  producing <name>.pmrem.ktx2. This is the same processing the Needle build pipeline
4117
4137
  applies to EXR textures inside GLB/GLTF files, exposed as a standalone command for
4118
4138
  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();
4139
+ `).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 }) => {
4140
+ await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
4121
4141
  let input = args.input.toString();
4122
4142
  existsSync13(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
4123
4143
  let inputFiles = [], inputIsDirectory = isDirectory(input);
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.1",
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": {