@hvgllc/hvgerp-mcp 3.4.0 → 3.5.0

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/hvgerp-mcp.mjs CHANGED
@@ -3115,6 +3115,7 @@ var require_utils = __commonJS({
3115
3115
  "use strict";
3116
3116
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3117
3117
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3118
+ var isPort = RegExp.prototype.test.bind(/^\d*$/u);
3118
3119
  var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3119
3120
  var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3120
3121
  var isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
@@ -3580,8 +3581,12 @@ var require_utils = __commonJS({
3580
3581
  uriTokens.push(host);
3581
3582
  }
3582
3583
  if (typeof component.port === "number" || typeof component.port === "string") {
3584
+ const port = String(component.port);
3585
+ if (!isPort(port)) {
3586
+ throw new TypeError("URI port is malformed.");
3587
+ }
3583
3588
  uriTokens.push(":");
3584
- uriTokens.push(String(component.port));
3589
+ uriTokens.push(port);
3585
3590
  }
3586
3591
  return uriTokens.length ? uriTokens.join("") : void 0;
3587
3592
  }
@@ -4024,12 +4029,15 @@ var require_fast_uri = __commonJS({
4024
4029
  }
4025
4030
  return false;
4026
4031
  }
4032
+ function isIPLiteral(host) {
4033
+ return host[0] === "[" && host[host.length - 1] === "]";
4034
+ }
4027
4035
  function hasMalformedComponentPercentEncoding(matches) {
4028
4036
  const host = matches[4];
4029
- return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !(host[0] === "[" && host[host.length - 1] === "]") && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4037
+ return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4030
4038
  }
4031
4039
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
4032
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && parsed.host[0] !== "[" && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4040
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4033
4041
  try {
4034
4042
  parsed.host = new URL("http://" + parsed.host).hostname;
4035
4043
  } catch (e) {
@@ -4116,10 +4124,11 @@ var require_fast_uri = __commonJS({
4116
4124
  if (parsed.host) {
4117
4125
  const ipv4result = isIPv4(parsed.host);
4118
4126
  if (ipv4result === false) {
4119
- const bracketedIPLiteral = parsed.host[0] === "[" && parsed.host[parsed.host.length - 1] === "]";
4127
+ const bracketedIPLiteral = isIPLiteral(parsed.host);
4128
+ const hasIPLiteralBracket = parsed.host.indexOf("[") !== -1 || parsed.host.indexOf("]") !== -1;
4120
4129
  const ipv6result = normalizeIPv6(parsed.host);
4121
4130
  isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
4122
- malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true;
4131
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4123
4132
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4124
4133
  if (malformedIPLiteral) {
4125
4134
  parsed.error = parsed.error || "URI host is malformed.";
@@ -4142,7 +4151,9 @@ var require_fast_uri = __commonJS({
4142
4151
  parsed.error = parsed.error || "URI is not a " + options.reference + " reference.";
4143
4152
  }
4144
4153
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
4145
- malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4154
+ if (!malformedIPLiteral) {
4155
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4156
+ }
4146
4157
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4147
4158
  if (uri.indexOf("%") !== -1) {
4148
4159
  if (parsed.host !== void 0 && !malformedIPLiteral) {
@@ -22951,9 +22962,9 @@ function cloneIssues(issues) {
22951
22962
  }
22952
22963
  var recursive = /* @__PURE__ */ new WeakMap();
22953
22964
  function isRecursive(inst, stack) {
22954
- const cached3 = recursive.get(inst);
22955
- if (cached3 !== void 0)
22956
- return cached3;
22965
+ const cached2 = recursive.get(inst);
22966
+ if (cached2 !== void 0)
22967
+ return cached2;
22957
22968
  if (stack.has(inst))
22958
22969
  return true;
22959
22970
  stack.add(inst);
@@ -32943,13 +32954,14 @@ var Hono = class _Hono {
32943
32954
  const allMethods = [...METHODS, METHOD_NAME_ALL_LOWERCASE];
32944
32955
  allMethods.forEach((method) => {
32945
32956
  this[method] = (args1, ...args) => {
32957
+ const methodName = method.toUpperCase();
32946
32958
  if (typeof args1 === "string") {
32947
32959
  this.#path = args1;
32948
32960
  } else {
32949
- this.#addRoute(method, this.#path, args1);
32961
+ this.#addRoute(methodName, this.#path, args1);
32950
32962
  }
32951
32963
  args.forEach((handler) => {
32952
- this.#addRoute(method, this.#path, handler);
32964
+ this.#addRoute(methodName, this.#path, handler);
32953
32965
  });
32954
32966
  return this;
32955
32967
  };
@@ -32958,9 +32970,10 @@ var Hono = class _Hono {
32958
32970
  for (const p of [path].flat()) {
32959
32971
  this.#path = p;
32960
32972
  for (const m of [method].flat()) {
32961
- handlers.map((handler) => {
32962
- this.#addRoute(m.toUpperCase(), this.#path, handler);
32963
- });
32973
+ const methodName = m.toUpperCase();
32974
+ for (const handler of handlers) {
32975
+ this.#addRoute(methodName, this.#path, handler);
32976
+ }
32964
32977
  }
32965
32978
  }
32966
32979
  return this;
@@ -33161,7 +33174,6 @@ var Hono = class _Hono {
33161
33174
  return this;
33162
33175
  }
33163
33176
  #addRoute(method, path, handler, baseRoutePath) {
33164
- method = method.toUpperCase();
33165
33177
  path = mergePath(this._basePath, path);
33166
33178
  const r = {
33167
33179
  basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
@@ -35414,88 +35426,48 @@ var decoder = new TextDecoder();
35414
35426
  var strictDecoder = new TextDecoder("utf-8", { fatal: true });
35415
35427
  var MAX_INT32 = 2 ** 32;
35416
35428
  function concat(...buffers) {
35417
- const size = buffers.reduce((acc, { length }) => acc + length, 0);
35418
- const buf = new Uint8Array(size);
35429
+ const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size);
35419
35430
  let i = 0;
35420
- for (const buffer of buffers) {
35421
- buf.set(buffer, i);
35422
- i += buffer.length;
35423
- }
35431
+ for (const buffer of buffers)
35432
+ buf.set(buffer, i), i += buffer.length;
35424
35433
  return buf;
35425
35434
  }
35435
+ var NON_ASCII = /[^\x00-\x7f]/;
35426
35436
  function encode3(string3) {
35437
+ if (typeof string3 == "string" && string3.length >= 128) {
35438
+ if (NON_ASCII.test(string3))
35439
+ throw new TypeError("non-ASCII string encountered in encode()");
35440
+ return encoder.encode(string3);
35441
+ }
35427
35442
  const bytes = new Uint8Array(string3.length);
35428
35443
  for (let i = 0; i < string3.length; i++) {
35429
35444
  const code = string3.charCodeAt(i);
35430
- if (code > 127) {
35445
+ if (code > 127)
35431
35446
  throw new TypeError("non-ASCII string encountered in encode()");
35432
- }
35433
35447
  bytes[i] = code;
35434
35448
  }
35435
35449
  return bytes;
35436
35450
  }
35437
-
35438
- // node_modules/jose/dist/webapi/lib/crypto_key.js
35439
- var unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
35440
- function checkUsage(key, usage) {
35441
- if (usage && !key.usages.includes(usage)) {
35442
- throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
35443
- }
35444
- }
35445
- function checkModulusLength(alg, key) {
35446
- const { modulusLength } = key.algorithm;
35447
- if (typeof modulusLength !== "number" || modulusLength < 2048) {
35448
- throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
35449
- }
35450
- }
35451
- function checkCryptoKey(key, expected, usage) {
35452
- const algorithm = key.algorithm;
35453
- if (algorithm.name !== expected.name) {
35454
- throw unusable(expected.name);
35455
- }
35456
- if (expected.hash && algorithm.hash?.name !== expected.hash) {
35457
- throw unusable(expected.hash, "algorithm.hash");
35458
- }
35459
- if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) {
35460
- throw unusable(expected.namedCurve, "algorithm.namedCurve");
35461
- }
35462
- if (expected.length !== void 0 && algorithm.length !== expected.length) {
35463
- throw unusable(expected.length, "algorithm.length");
35464
- }
35465
- checkUsage(key, usage);
35466
- }
35467
-
35468
- // node_modules/jose/dist/webapi/lib/invalid_key_input.js
35469
- function message(msg, actual, ...types) {
35470
- if (types.length > 2) {
35471
- const last = types.pop();
35472
- msg += `one of type ${types.join(", ")}, or ${last}.`;
35473
- } else if (types.length === 2) {
35474
- msg += `one of type ${types[0]} or ${types[1]}.`;
35475
- } else {
35476
- msg += `of type ${types[0]}.`;
35477
- }
35478
- if (actual == null) {
35479
- msg += ` Received ${actual}`;
35480
- } else if (typeof actual === "function" && actual.name) {
35481
- msg += ` Received function ${actual.name}`;
35482
- } else if (typeof actual === "object" && actual != null) {
35483
- if (actual.constructor?.name) {
35484
- msg += ` Received an instance of ${actual.constructor.name}`;
35485
- }
35451
+ function decodeBase64(encoded, url = false) {
35452
+ if (Uint8Array.fromBase64)
35453
+ return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" });
35454
+ if (url) {
35455
+ if (encoded.includes("+") || encoded.includes("/"))
35456
+ throw new TypeError("Invalid base64url");
35457
+ encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
35486
35458
  }
35487
- return msg;
35459
+ const binary = atob(encoded), bytes = new Uint8Array(binary.length);
35460
+ for (let i = 0; i < binary.length; i++)
35461
+ bytes[i] = binary.charCodeAt(i);
35462
+ return bytes;
35488
35463
  }
35489
- var withAlg = (alg, actual, ...types) => message(`Key for the ${alg} algorithm must be `, actual, ...types);
35490
35464
 
35491
35465
  // node_modules/jose/dist/webapi/util/errors.js
35492
35466
  var JOSEError = class extends Error {
35493
35467
  static code = "ERR_JOSE_GENERIC";
35494
35468
  code = "ERR_JOSE_GENERIC";
35495
35469
  constructor(message2, options) {
35496
- super(message2, options);
35497
- this.name = this.constructor.name;
35498
- Error.captureStackTrace?.(this, this.constructor);
35470
+ super(message2, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor);
35499
35471
  }
35500
35472
  };
35501
35473
  var JWTClaimValidationFailed = class extends JOSEError {
@@ -35505,10 +35477,7 @@ var JWTClaimValidationFailed = class extends JOSEError {
35505
35477
  reason;
35506
35478
  payload;
35507
35479
  constructor(message2, payload, claim2 = "unspecified", reason = "unspecified") {
35508
- super(message2, { cause: { claim: claim2, reason, payload } });
35509
- this.claim = claim2;
35510
- this.reason = reason;
35511
- this.payload = payload;
35480
+ super(message2, { cause: { claim: claim2, reason, payload } }), this.claim = claim2, this.reason = reason, this.payload = payload;
35512
35481
  }
35513
35482
  };
35514
35483
  var JWTExpired = class extends JOSEError {
@@ -35518,10 +35487,7 @@ var JWTExpired = class extends JOSEError {
35518
35487
  reason;
35519
35488
  payload;
35520
35489
  constructor(message2, payload, claim2 = "unspecified", reason = "unspecified") {
35521
- super(message2, { cause: { claim: claim2, reason, payload } });
35522
- this.claim = claim2;
35523
- this.reason = reason;
35524
- this.payload = payload;
35490
+ super(message2, { cause: { claim: claim2, reason, payload } }), this.claim = claim2, this.reason = reason, this.payload = payload;
35525
35491
  }
35526
35492
  };
35527
35493
  var JOSEAlgNotAllowed = class extends JOSEError {
@@ -35575,73 +35541,37 @@ var JWSSignatureVerificationFailed = class extends JOSEError {
35575
35541
  }
35576
35542
  };
35577
35543
 
35578
- // node_modules/jose/dist/webapi/lib/is_key_like.js
35579
- var isCryptoKey = (key) => {
35580
- if (key?.[Symbol.toStringTag] === "CryptoKey")
35581
- return true;
35582
- try {
35583
- return key instanceof CryptoKey;
35584
- } catch {
35585
- return false;
35586
- }
35587
- };
35588
- var isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject";
35589
- var isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
35590
-
35591
- // node_modules/jose/dist/webapi/lib/base64.js
35592
- function decodeBase64(encoded) {
35593
- if (Uint8Array.fromBase64) {
35594
- return Uint8Array.fromBase64(encoded);
35595
- }
35596
- const binary = atob(encoded);
35597
- const bytes = new Uint8Array(binary.length);
35598
- for (let i = 0; i < binary.length; i++) {
35599
- bytes[i] = binary.charCodeAt(i);
35600
- }
35601
- return bytes;
35602
- }
35603
-
35604
35544
  // node_modules/jose/dist/webapi/util/base64url.js
35605
35545
  var invalid = "The input to be decoded is not correctly encoded.";
35606
35546
  function decode3(input) {
35607
- if (Uint8Array.fromBase64) {
35608
- try {
35609
- return Uint8Array.fromBase64(typeof input === "string" ? input : decoder.decode(input), {
35610
- alphabet: "base64url"
35611
- });
35612
- } catch (cause) {
35613
- throw new TypeError(invalid, { cause });
35614
- }
35615
- }
35616
- let encoded = input;
35617
- if (encoded instanceof Uint8Array) {
35618
- encoded = decoder.decode(encoded);
35619
- }
35620
- if (encoded.includes("+") || encoded.includes("/")) {
35621
- throw new TypeError(invalid);
35622
- }
35623
- encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
35624
35547
  try {
35625
- return decodeBase64(encoded);
35626
- } catch {
35627
- throw new TypeError(invalid);
35548
+ return decodeBase64(typeof input == "string" ? input : decoder.decode(input), true);
35549
+ } catch (cause) {
35550
+ throw new TypeError(invalid, { cause });
35628
35551
  }
35629
35552
  }
35630
35553
 
35631
- // node_modules/jose/dist/webapi/lib/type_checks.js
35554
+ // node_modules/jose/dist/webapi/lib/validate.js
35632
35555
  function isObject2(input) {
35633
- if (typeof input !== "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") {
35556
+ if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]")
35634
35557
  return false;
35635
- }
35636
35558
  const prototype = Object.getPrototypeOf(input);
35637
35559
  return prototype === null || Object.getPrototypeOf(prototype) === null;
35638
35560
  }
35639
35561
  function isJwkSet(input) {
35640
35562
  return isObject2(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject2);
35641
35563
  }
35642
-
35643
- // node_modules/jose/dist/webapi/lib/helpers.js
35644
- var unprotected = Symbol();
35564
+ function isDisjoint(...headers) {
35565
+ const parameters = /* @__PURE__ */ new Set();
35566
+ for (const header of headers)
35567
+ if (header)
35568
+ for (const parameter of Object.keys(header)) {
35569
+ if (parameters.has(parameter))
35570
+ return false;
35571
+ parameters.add(parameter);
35572
+ }
35573
+ return true;
35574
+ }
35645
35575
  function decodeBase64url(value, label, ErrorClass) {
35646
35576
  try {
35647
35577
  return decode3(value);
@@ -35663,48 +35593,42 @@ function parseJoseHeader(b64, ErrorClass, message2) {
35663
35593
  } catch {
35664
35594
  throw new ErrorClass(message2);
35665
35595
  }
35666
- if (!isObject2(parsed)) {
35596
+ if (!isObject2(parsed))
35667
35597
  throw new ErrorClass(message2);
35668
- }
35669
35598
  return parsed;
35670
35599
  }
35671
-
35672
- // node_modules/jose/dist/webapi/lib/jwk_to_key.js
35673
- async function jwkToKey(entry, jwk) {
35674
- if (jwk.kty === "RSA" && "oth" in jwk && jwk.oth !== void 0) {
35675
- throw new JOSENotSupported('RSA JWK "oth" (Other Primes Info) Parameter value is not supported');
35676
- }
35677
- if (!entry.kty.includes(jwk.kty)) {
35678
- throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
35679
- }
35680
- const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle;
35681
- const isPrivate = !!(jwk.d || jwk.priv);
35682
- const keyData = { ...jwk };
35683
- if (keyData.kty !== "AKP") {
35684
- delete keyData.alg;
35685
- }
35686
- delete keyData.use;
35687
- return crypto.subtle.importKey("jwk", keyData, algorithm, jwk.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]);
35688
- }
35689
-
35690
- // node_modules/jose/dist/webapi/lib/jwk_metadata.js
35691
- function snapshotJwk(jwk) {
35692
- return { __proto__: null, ...jwk };
35600
+ var JWS_RECOGNIZED = { __proto__: null, b64: true };
35601
+ function validateAlgorithms(option, algorithms) {
35602
+ if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string")))
35603
+ throw new TypeError(`"${option}" option must be an array of strings`);
35604
+ return algorithms === void 0 ? void 0 : new Set(algorithms);
35693
35605
  }
35694
- function normalizeJwk(jwk) {
35695
- const normalized = snapshotJwk(jwk);
35696
- if (normalized.ext !== void 0 && typeof normalized.ext !== "boolean") {
35697
- throw new TypeError('"ext" (Extractable) Parameter must be a boolean');
35606
+ function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
35607
+ if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0)
35608
+ throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
35609
+ if (!protectedHeader || protectedHeader.crit === void 0)
35610
+ return [];
35611
+ if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0))
35612
+ throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
35613
+ const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault };
35614
+ for (const parameter of protectedHeader.crit) {
35615
+ if (!(parameter in recognized))
35616
+ throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
35617
+ if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0)
35618
+ throw new Err(`Extension Header Parameter "${parameter}" is missing`);
35619
+ if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0))
35620
+ throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
35698
35621
  }
35699
- if (normalized.key_ops !== void 0) {
35700
- const value = normalized.key_ops;
35701
- const keyOps = Array.isArray(value) ? [...value] : void 0;
35702
- if (!keyOps || keyOps.some((operation) => typeof operation !== "string") || new Set(keyOps).size !== keyOps.length) {
35703
- throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings');
35704
- }
35705
- normalized.key_ops = keyOps;
35622
+ return protectedHeader.crit;
35623
+ }
35624
+ function validateB64(protectedHeader, extensions) {
35625
+ if (extensions.includes("b64")) {
35626
+ const b64 = protectedHeader.b64;
35627
+ if (typeof b64 != "boolean")
35628
+ throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
35629
+ return b64;
35706
35630
  }
35707
- return normalized;
35631
+ return true;
35708
35632
  }
35709
35633
 
35710
35634
  // node_modules/jose/dist/webapi/lib/key.js
@@ -35713,60 +35637,55 @@ var jwkMatchesOp = (entry, key, usage) => {
35713
35637
  const { alg } = entry;
35714
35638
  if (key.use !== void 0) {
35715
35639
  const expected = usage === "sign" || usage === "verify" ? "sig" : "enc";
35716
- if (key.use !== expected) {
35640
+ if (key.use !== expected)
35717
35641
  throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
35718
- }
35719
35642
  }
35720
- if (key.alg !== void 0 && key.alg !== alg) {
35643
+ if (key.alg !== void 0 && key.alg !== alg)
35721
35644
  throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
35722
- }
35723
35645
  if (Array.isArray(key.key_ops)) {
35724
35646
  const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage;
35725
- if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) {
35647
+ if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp))
35726
35648
  throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
35727
- }
35728
35649
  }
35729
35650
  };
35730
- function checkKeyType(entry, key, usage) {
35731
- const { alg, secret } = entry;
35732
- const privateKey = usage === "decrypt" || usage === "sign";
35651
+ async function prepareKey(entry, key, usage) {
35652
+ const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign";
35733
35653
  if (secret && key instanceof Uint8Array)
35734
- return [BYTES, key];
35654
+ return key;
35655
+ let normalized, keyObject;
35735
35656
  if (isObject2(key)) {
35736
- const normalized = normalizeJwk(key);
35737
- if (typeof normalized.kty !== "string") {
35738
- throw new TypeError(secret ? withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key", "Uint8Array") : withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key"));
35739
- }
35740
- const valid = secret ? normalized.kty === "oct" && typeof normalized.k === "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv === "string" || typeof normalized.d === "string" : normalized.d === void 0 && normalized.priv === void 0);
35741
- if (!valid) {
35742
- throw new TypeError(secret ? `JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present` : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`);
35743
- }
35744
- jwkMatchesOp(entry, normalized, usage);
35745
- return [JWK, key, normalized];
35746
- }
35747
- if (!isKeyLike(key)) {
35748
- throw new TypeError(secret ? withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key", "Uint8Array") : withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key"));
35749
- }
35750
- if (secret) {
35751
- if (key.type !== "secret") {
35752
- throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
35657
+ if (normalized = normalizeJwk(key), typeof normalized.kty != "string")
35658
+ throw invalidKeyType(alg, key, secret);
35659
+ if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0)))
35660
+ throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`);
35661
+ if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct")
35662
+ return decode3(normalized.k);
35663
+ if (!Object.isFrozen(key)) {
35664
+ const { key_ops } = key;
35665
+ Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key);
35753
35666
  }
35754
35667
  } else {
35755
- if (key.type === "secret") {
35756
- throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
35757
- }
35758
- const expectedType = privateKey ? "private" : "public";
35759
- if ((key.type === "public" || key.type === "private") && key.type !== expectedType) {
35760
- const operation = usage === "sign" ? "signing" : usage === "verify" ? "verifying" : `${usage.slice(0, -1)}tion`;
35761
- throw new TypeError(`${tag(key)} instances for asymmetric algorithm ${operation} must be of type "${expectedType}"`);
35762
- }
35668
+ if (!isKeyLike(key))
35669
+ throw invalidKeyType(alg, key, secret);
35670
+ const expectedType = secret ? "secret" : privateKey ? "private" : "public";
35671
+ if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type)))
35672
+ throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`);
35673
+ if (isCryptoKey(key))
35674
+ return key;
35675
+ if (keyObject = key, keyObject.type === "secret")
35676
+ return keyObject.export();
35763
35677
  }
35764
- return isCryptoKey(key) ? [CRYPTO, key] : [KEYOBJECT, key];
35678
+ cache ||= /* @__PURE__ */ new WeakMap();
35679
+ const cacheKey2 = key;
35680
+ let cached2 = cache.get(cacheKey2);
35681
+ if (cached2?.[alg])
35682
+ return cached2[alg];
35683
+ if (cached2 || cache.set(cacheKey2, cached2 = {}), keyObject && typeof keyObject.toCryptoKey == "function") {
35684
+ const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle;
35685
+ return cached2[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]);
35686
+ }
35687
+ return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached2[alg] = await jwkToKey(entry, normalized);
35765
35688
  }
35766
- var BYTES = 0;
35767
- var CRYPTO = 1;
35768
- var KEYOBJECT = 2;
35769
- var JWK = 3;
35770
35689
  var cache;
35771
35690
  var nist = {
35772
35691
  __proto__: null,
@@ -35774,139 +35693,83 @@ var nist = {
35774
35693
  secp384r1: "P-384",
35775
35694
  secp521r1: "P-521"
35776
35695
  };
35777
- function cached2(key, alg, value) {
35778
- cache ||= /* @__PURE__ */ new WeakMap();
35779
- const entry = cache.get(key);
35780
- if (value) {
35781
- if (entry) {
35782
- entry[alg] = value;
35783
- } else {
35784
- cache.set(key, { [alg]: value });
35785
- }
35696
+ var isCryptoKey = (key) => {
35697
+ if (key?.[Symbol.toStringTag] === "CryptoKey")
35698
+ return true;
35699
+ try {
35700
+ return key instanceof CryptoKey;
35701
+ } catch {
35702
+ return false;
35786
35703
  }
35787
- return value ?? entry?.[alg];
35788
- }
35789
- var handleJWK = async (key, jwk, entry) => cached2(key, entry.alg) ?? cached2(key, entry.alg, await jwkToKey(entry, { ...jwk, alg: entry.alg }));
35790
- var handleKeyObject = (keyObject, entry) => {
35791
- const hit = cached2(keyObject, entry.alg);
35792
- if (hit)
35793
- return hit;
35794
- const isPublic = keyObject.type === "public";
35795
- const usages = entry.usages[isPublic ? 0 : 1];
35796
- const { asymmetricKeyType } = keyObject;
35797
- const crv = nist[keyObject.asymmetricKeyDetails?.namedCurve];
35798
- const params = entry.resolve?.({ crv, asymmetricKeyType }) ?? entry.subtle;
35799
- return cached2(keyObject, entry.alg, keyObject.toCryptoKey(params, isPublic, usages));
35800
35704
  };
35801
- async function prepareKey(entry, key, usage) {
35802
- const tagged = checkKeyType(entry, key, usage);
35803
- switch (tagged[0]) {
35804
- case BYTES:
35805
- case CRYPTO:
35806
- return tagged[1];
35807
- case JWK: {
35808
- const key2 = tagged[1];
35809
- const normalized = tagged[2];
35810
- if (normalized.kty === "oct") {
35811
- return decode3(normalized.k);
35812
- }
35813
- if (!Object.isFrozen(key2)) {
35814
- const { key_ops } = key2;
35815
- if (Array.isArray(key_ops))
35816
- Object.freeze(key_ops);
35817
- Object.freeze(key2);
35818
- }
35819
- return handleJWK(key2, normalized, entry);
35820
- }
35821
- case KEYOBJECT: {
35822
- const keyObject = tagged[1];
35823
- if (keyObject.type === "secret") {
35824
- return keyObject.export();
35825
- }
35826
- if ("toCryptoKey" in keyObject && typeof keyObject.toCryptoKey === "function") {
35827
- return handleKeyObject(keyObject, entry);
35828
- }
35829
- return handleJWK(keyObject, keyObject.export({ format: "jwk" }), entry);
35830
- }
35705
+ var isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject";
35706
+ var isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
35707
+ function message(msg, actual, ...types) {
35708
+ if (types.length > 2) {
35709
+ const last = types.pop();
35710
+ msg += `one of type ${types.join(", ")}, or ${last}.`;
35711
+ } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`;
35712
+ return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg;
35713
+ }
35714
+ function invalidKeyType(alg, actual, secret) {
35715
+ const types = ["CryptoKey", "KeyObject", "JSON Web Key"];
35716
+ return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types));
35717
+ }
35718
+ var unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
35719
+ function checkUsage(key, usage) {
35720
+ if (usage && !key.usages.includes(usage))
35721
+ throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
35722
+ }
35723
+ function checkModulusLength(alg, key) {
35724
+ const { modulusLength } = key.algorithm;
35725
+ if (typeof modulusLength != "number" || modulusLength < 2048)
35726
+ throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
35727
+ }
35728
+ function checkCryptoKey(key, expected, usage) {
35729
+ const algorithm = key.algorithm;
35730
+ if (algorithm.name !== expected.name)
35731
+ throw unusable(expected.name);
35732
+ if (expected.hash && algorithm.hash?.name !== expected.hash)
35733
+ throw unusable(expected.hash, "algorithm.hash");
35734
+ if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve)
35735
+ throw unusable(expected.namedCurve, "algorithm.namedCurve");
35736
+ if (expected.length !== void 0 && algorithm.length !== expected.length)
35737
+ throw unusable(expected.length, "algorithm.length");
35738
+ checkUsage(key, usage);
35739
+ }
35740
+ function snapshotJwk(jwk) {
35741
+ return { __proto__: null, ...jwk };
35742
+ }
35743
+ function normalizeJwk(jwk) {
35744
+ const normalized = snapshotJwk(jwk);
35745
+ if (normalized.ext !== void 0 && typeof normalized.ext != "boolean")
35746
+ throw new TypeError('"ext" (Extractable) Parameter must be a boolean');
35747
+ if (normalized.key_ops !== void 0) {
35748
+ const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0;
35749
+ if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length)
35750
+ throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings');
35751
+ normalized.key_ops = keyOps;
35831
35752
  }
35753
+ return normalized;
35754
+ }
35755
+ async function jwkToKey(entry, jwk, extractable) {
35756
+ if (!entry.kty.includes(jwk.kty))
35757
+ throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value');
35758
+ const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext };
35759
+ return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]);
35760
+ }
35761
+ async function rawKey(key, expected, usage, extractable = false) {
35762
+ return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key;
35832
35763
  }
35833
35764
 
35834
35765
  // node_modules/jose/dist/webapi/lib/key_descriptor.js
35835
35766
  function table(entries) {
35836
35767
  const out = { __proto__: null };
35837
- for (const alg in entries) {
35768
+ for (const alg in entries)
35838
35769
  out[alg] = { ...entries[alg], alg };
35839
- }
35840
35770
  return out;
35841
35771
  }
35842
35772
 
35843
- // node_modules/jose/dist/webapi/lib/options.js
35844
- var JWS_RECOGNIZED = { __proto__: null, b64: true };
35845
- function validateAlgorithms(option, algorithms) {
35846
- if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== "string"))) {
35847
- throw new TypeError(`"${option}" option must be an array of strings`);
35848
- }
35849
- if (!algorithms) {
35850
- return void 0;
35851
- }
35852
- return new Set(algorithms);
35853
- }
35854
- function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
35855
- if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) {
35856
- throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected');
35857
- }
35858
- if (!protectedHeader || protectedHeader.crit === void 0) {
35859
- return [];
35860
- }
35861
- if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== "string" || input.length === 0)) {
35862
- throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present');
35863
- }
35864
- const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault };
35865
- for (const parameter of protectedHeader.crit) {
35866
- if (!(parameter in recognized)) {
35867
- throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
35868
- }
35869
- if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) {
35870
- throw new Err(`Extension Header Parameter "${parameter}" is missing`);
35871
- }
35872
- if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) {
35873
- throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
35874
- }
35875
- }
35876
- return protectedHeader.crit;
35877
- }
35878
- function validateB64(protectedHeader, extensions) {
35879
- if (extensions.includes("b64")) {
35880
- const b64 = protectedHeader.b64;
35881
- if (typeof b64 !== "boolean") {
35882
- throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean');
35883
- }
35884
- return b64;
35885
- }
35886
- return true;
35887
- }
35888
-
35889
- // node_modules/jose/dist/webapi/lib/signing.js
35890
- async function getSigKey(entry, key, usage) {
35891
- if (key instanceof Uint8Array) {
35892
- return crypto.subtle.importKey("raw", key, entry.subtle, false, [
35893
- usage
35894
- ]);
35895
- }
35896
- checkCryptoKey(key, entry.subtle, usage);
35897
- if (entry.minRsaBits)
35898
- checkModulusLength(entry.alg, key);
35899
- return key;
35900
- }
35901
- async function verify(entry, key, signature, data) {
35902
- const cryptoKey = await getSigKey(entry, key, "verify");
35903
- try {
35904
- return await crypto.subtle.verify(entry.signing, cryptoKey, signature, data);
35905
- } catch {
35906
- return false;
35907
- }
35908
- }
35909
-
35910
35773
  // node_modules/jose/dist/webapi/lib/jws_algorithms.js
35911
35774
  var sig = [["verify"], ["sign"]];
35912
35775
  function hmac(bits) {
@@ -35914,8 +35777,7 @@ function hmac(bits) {
35914
35777
  return { kty: ["oct"], secret: true, subtle, signing: subtle, usages: sig };
35915
35778
  }
35916
35779
  function rsa(bits, saltLength) {
35917
- const name = saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5";
35918
- const subtle = { name, hash: `SHA-${bits}` };
35780
+ const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` };
35919
35781
  return {
35920
35782
  kty: ["RSA"],
35921
35783
  subtle,
@@ -35944,8 +35806,7 @@ function eddsa() {
35944
35806
  };
35945
35807
  }
35946
35808
  function mldsa(bits) {
35947
- const name = `ML-DSA-${bits}`;
35948
- const subtle = { name };
35809
+ const subtle = { name: `ML-DSA-${bits}` };
35949
35810
  return {
35950
35811
  kty: ["AKP"],
35951
35812
  subtle,
@@ -35973,10 +35834,9 @@ var JWS = table({
35973
35834
  "ML-DSA-87": mldsa(87)
35974
35835
  });
35975
35836
  function jwsAlgorithm(alg) {
35976
- const entry = typeof alg === "string" ? JWS[alg] : void 0;
35977
- if (!entry) {
35837
+ const entry = typeof alg == "string" ? JWS[alg] : void 0;
35838
+ if (!entry)
35978
35839
  throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
35979
- }
35980
35840
  return entry;
35981
35841
  }
35982
35842
 
@@ -35984,19 +35844,8 @@ function jwsAlgorithm(alg) {
35984
35844
  function prepareVerify(options) {
35985
35845
  return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit];
35986
35846
  }
35987
- function parseProtectedHeader(encodedProtected, parsedProtected = encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid")) {
35988
- return parsedProtected;
35989
- }
35990
- function validateJwsHeaders(parsedProt, joseHeader, shared) {
35991
- const b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader));
35992
- const alg = joseHeader.alg;
35993
- if (typeof alg !== "string" || !alg) {
35994
- throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
35995
- }
35996
- if (shared[0] && !shared[0].has(alg)) {
35997
- throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
35998
- }
35999
- return [b64, alg];
35847
+ function parseProtectedHeader(encodedProtected) {
35848
+ return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid");
36000
35849
  }
36001
35850
  function encodeCompactUnencodedPayload(payload) {
36002
35851
  try {
@@ -36005,39 +35854,42 @@ function encodeCompactUnencodedPayload(payload) {
36005
35854
  throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters");
36006
35855
  }
36007
35856
  }
36008
- async function verifyPrepared(jws, shared, key, encodedProtected, parsedProt, alg, signingPayload) {
35857
+ async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) {
35858
+ const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected);
35859
+ if (!isDisjoint(parsedProt, header))
35860
+ throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
35861
+ const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader;
35862
+ if (typeof alg != "string" || !alg)
35863
+ throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
35864
+ if (shared[0] && !shared[0].has(alg))
35865
+ throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed');
35866
+ if (b64) {
35867
+ if (typeof inputPayload != "string")
35868
+ throw new JWSInvalid("JWS Payload must be a string");
35869
+ } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array))
35870
+ throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
35871
+ const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload);
36009
35872
  let resolvedKey = false;
36010
- if (typeof key === "function") {
36011
- key = await key(parsedProt, jws);
36012
- resolvedKey = true;
36013
- }
36014
- const b64 = typeof signingPayload === "string";
36015
- const entry = jwsAlgorithm(alg);
36016
- const data = concat(encodedProtected !== void 0 ? encode3(encodedProtected) : new Uint8Array(), encode3("."), b64 ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload);
36017
- const signature = decodeBase64url(jws.signature, "signature", JWSInvalid);
36018
- const k = await prepareKey(entry, key, "verify");
36019
- if (!await verify(entry, k, signature, data)) {
36020
- throw new JWSSignatureVerificationFailed();
35873
+ typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = true);
35874
+ const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode3(encodedProtected) : new Uint8Array(), encode3("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify");
35875
+ entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey);
35876
+ let verified = false;
35877
+ try {
35878
+ verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data);
35879
+ } catch {
36021
35880
  }
36022
- const payload = b64 ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload;
36023
- return [payload, parsedProt, b64, k, resolvedKey];
35881
+ if (!verified)
35882
+ throw new JWSSignatureVerificationFailed();
35883
+ const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload };
35884
+ return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64];
36024
35885
  }
36025
35886
  async function verifyCompact(jws, shared, key) {
36026
- if (jws instanceof Uint8Array) {
36027
- jws = decoder.decode(jws);
36028
- }
36029
- if (typeof jws !== "string") {
35887
+ if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string")
36030
35888
  throw new JWSInvalid("Compact JWS must be a string or Uint8Array");
36031
- }
36032
35889
  const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split(".");
36033
- if (length !== 3) {
35890
+ if (length !== 3)
36034
35891
  throw new JWSInvalid("Invalid Compact JWS");
36035
- }
36036
- const compactJws = { payload, protected: protectedHeader, signature };
36037
- const parsedProt = parseProtectedHeader(protectedHeader);
36038
- const [b64, alg] = validateJwsHeaders(parsedProt, parsedProt, shared);
36039
- const signingPayload = b64 ? payload : encodeCompactUnencodedPayload(payload);
36040
- return verifyPrepared(compactJws, shared, key, protectedHeader, parsedProt, alg, signingPayload);
35892
+ return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload);
36041
35893
  }
36042
35894
 
36043
35895
  // node_modules/jose/dist/webapi/lib/jwt_claims_set.js
@@ -36056,50 +35908,29 @@ function invalidDuration() {
36056
35908
  throw new TypeError("Invalid time period format");
36057
35909
  }
36058
35910
  function secs(str) {
36059
- if (typeof str !== "string") {
36060
- invalidDuration();
36061
- }
35911
+ typeof str != "string" && invalidDuration();
36062
35912
  const matched = REGEX.exec(str);
36063
- if (!matched || matched[4] && matched[1]) {
36064
- invalidDuration();
36065
- }
36066
- const value = parseFloat(matched[2]);
36067
- const numericDate = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
36068
- if (!Number.isFinite(numericDate)) {
36069
- invalidDuration();
36070
- }
36071
- if (matched[1] === "-" || matched[4] === "ago") {
36072
- return -numericDate;
36073
- }
36074
- return numericDate;
35913
+ (!matched || matched[4] && matched[1]) && invalidDuration();
35914
+ const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
35915
+ return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2;
36075
35916
  }
36076
35917
  function validateInput(label, input) {
36077
- if (!Number.isFinite(input)) {
35918
+ if (!Number.isFinite(input))
36078
35919
  throw new TypeError(`Invalid ${label} input`);
36079
- }
36080
35920
  return input;
36081
35921
  }
36082
35922
  var normalizeTyp = (value) => {
36083
35923
  const normalized = value.toLowerCase();
36084
35924
  return value.includes("/") ? normalized : `application/${normalized}`;
36085
35925
  };
36086
- var checkAudiencePresence = (audPayload, audOption) => {
36087
- if (typeof audPayload === "string") {
36088
- return audOption.includes(audPayload);
36089
- }
36090
- if (Array.isArray(audPayload)) {
36091
- return audOption.some((aud) => audPayload.includes(aud));
36092
- }
36093
- return false;
36094
- };
35926
+ var checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : false;
36095
35927
  function validateNumericDate(payload, claim2, required2 = false) {
36096
35928
  const value = payload[claim2];
36097
- if (value === void 0 && !required2)
36098
- return void 0;
36099
- if (typeof value !== "number") {
36100
- throw new JWTClaimValidationFailed(`"${claim2}" claim must be a number`, payload, claim2, "invalid");
35929
+ if (!(value === void 0 && !required2)) {
35930
+ if (typeof value != "number")
35931
+ throw new JWTClaimValidationFailed(`"${claim2}" claim must be a number`, payload, claim2, "invalid");
35932
+ return value;
36101
35933
  }
36102
- return value;
36103
35934
  }
36104
35935
  function unexpectedClaim(payload, claim2) {
36105
35936
  throw new JWTClaimValidationFailed(`unexpected "${claim2}" claim value`, payload, claim2, checkFailed);
@@ -36110,107 +35941,70 @@ function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
36110
35941
  payload = JSON.parse(strictDecoder.decode(encodedPayload));
36111
35942
  } catch {
36112
35943
  }
36113
- if (!isObject2(payload)) {
35944
+ if (!isObject2(payload))
36114
35945
  throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
36115
- }
36116
35946
  const { typ } = options;
36117
- if (typ !== void 0 && (typeof protectedHeader.typ !== "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) {
35947
+ if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ)))
36118
35948
  throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed);
36119
- }
36120
- const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
36121
- const presenceCheck = [...requiredClaims];
36122
- if (maxTokenAge !== void 0)
36123
- presenceCheck.push("iat");
36124
- if (audience !== void 0)
36125
- presenceCheck.push("aud");
36126
- if (subject !== void 0)
36127
- presenceCheck.push("sub");
36128
- if (issuer !== void 0)
36129
- presenceCheck.push("iss");
36130
- for (const claim2 of new Set(presenceCheck.reverse())) {
36131
- if (!Object.hasOwn(payload, claim2)) {
35949
+ const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims];
35950
+ maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss");
35951
+ for (const claim2 of new Set(presenceCheck.reverse()))
35952
+ if (!Object.hasOwn(payload, claim2))
36132
35953
  throw new JWTClaimValidationFailed(`missing required "${claim2}" claim`, payload, claim2, "missing");
36133
- }
36134
- }
36135
- if (issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) {
36136
- unexpectedClaim(payload, "iss");
36137
- }
36138
- if (subject !== void 0 && payload.sub !== subject) {
36139
- unexpectedClaim(payload, "sub");
36140
- }
36141
- if (audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience === "string" ? [audience] : audience)) {
36142
- unexpectedClaim(payload, "aud");
36143
- }
35954
+ issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud");
36144
35955
  const { clockTolerance } = options;
36145
35956
  let tolerance = 0;
36146
- if (typeof clockTolerance === "string") {
35957
+ if (typeof clockTolerance == "string")
36147
35958
  tolerance = secs(clockTolerance);
36148
- } else if (clockTolerance !== void 0) {
36149
- if (typeof clockTolerance !== "number") {
35959
+ else if (clockTolerance !== void 0) {
35960
+ if (typeof clockTolerance != "number")
36150
35961
  throw new TypeError("Invalid clockTolerance option type");
36151
- }
36152
35962
  tolerance = clockTolerance;
36153
35963
  }
36154
35964
  validateInput("clockTolerance option", tolerance);
36155
- const { currentDate } = options;
36156
- const now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate));
36157
- const iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0);
36158
- const nbf = validateNumericDate(payload, "nbf");
36159
- if (nbf !== void 0) {
36160
- if (nbf > now + tolerance) {
36161
- throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
36162
- }
36163
- }
35965
+ const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf");
35966
+ if (nbf !== void 0 && nbf > now + tolerance)
35967
+ throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
36164
35968
  const exp = validateNumericDate(payload, "exp");
36165
- if (exp !== void 0) {
36166
- if (exp <= now - tolerance) {
36167
- throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
36168
- }
36169
- }
35969
+ if (exp !== void 0 && exp <= now - tolerance)
35970
+ throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
36170
35971
  if (maxTokenAge !== void 0) {
36171
- const age = now - iat;
36172
- const max = validateInput("maxTokenAge option", typeof maxTokenAge === "number" ? maxTokenAge : secs(maxTokenAge));
36173
- if (age - tolerance > max) {
35972
+ const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge));
35973
+ if (age - tolerance > max)
36174
35974
  throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed);
36175
- }
36176
- if (age < -tolerance) {
35975
+ if (age < -tolerance)
36177
35976
  throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed);
36178
- }
36179
35977
  }
36180
35978
  return payload;
36181
35979
  }
36182
35980
 
36183
35981
  // node_modules/jose/dist/webapi/jwt/verify.js
36184
35982
  async function jwtVerify(jwt, key, options) {
36185
- const verified = await verifyCompact(jwt, prepareVerify(options), key);
36186
- if (!verified[2]) {
35983
+ const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key);
35984
+ if (!b64)
36187
35985
  throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
36188
- }
36189
- const payload = validateClaimsSet(verified[1], verified[0], options);
36190
- const result = { payload, protectedHeader: verified[1] };
36191
- if (typeof key === "function") {
36192
- return { ...result, key: verified[3] };
36193
- }
36194
- return result;
35986
+ const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options);
35987
+ return { ...verified, payload };
36195
35988
  }
36196
35989
 
36197
35990
  // node_modules/jose/dist/webapi/jwks/local.js
36198
35991
  function isUsableJWK(jwk, entry, alg, kid) {
36199
- const { kty, key_ops, ext, kid: jwkKid, alg: jwkAlg, use, crv } = snapshotJwk(jwk);
36200
- const keyOps = Array.isArray(key_ops) ? [...key_ops] : key_ops;
36201
- return (ext === void 0 || typeof ext === "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation === "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid === "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv);
35992
+ const { kty, key_ops: keyOps, ext, kid: jwkKid, alg: jwkAlg, use, crv } = jwk;
35993
+ return (ext === void 0 || typeof ext == "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation == "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid == "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv);
36202
35994
  }
36203
35995
  async function importWithAlgCache(cache2, jwk, entry) {
36204
- const cached3 = cache2.get(jwk) || cache2.set(jwk, {}).get(jwk);
36205
- const { alg } = entry;
36206
- if (cached3[alg] === void 0) {
36207
- const key = await jwkToKey(entry, { ...jwk, alg, ext: true });
36208
- if (key.type !== "public") {
36209
- throw new JWKSInvalid("JSON Web Key Set members must be public keys");
36210
- }
36211
- cached3[alg] = key;
35996
+ const cached2 = cache2.get(jwk) || cache2.set(jwk, {}).get(jwk), { alg } = entry;
35997
+ if (cached2[alg] === void 0) {
35998
+ const pending = jwkToKey(entry, jwk, true).then((key) => {
35999
+ if (key.type !== "public")
36000
+ throw new JWKSInvalid("JSON Web Key Set members must be public keys");
36001
+ return cached2[alg] = key, key;
36002
+ }).catch((error2) => {
36003
+ throw cached2[alg] === pending && delete cached2[alg], error2;
36004
+ });
36005
+ cached2[alg] = pending;
36212
36006
  }
36213
- return cached3[alg];
36007
+ return cached2[alg];
36214
36008
  }
36215
36009
  function createLocalJWKSet(jwks) {
36216
36010
  let snapshot;
@@ -36218,51 +36012,42 @@ function createLocalJWKSet(jwks) {
36218
36012
  snapshot = structuredClone(jwks);
36219
36013
  } catch {
36220
36014
  }
36221
- if (!isJwkSet(snapshot)) {
36015
+ if (!isJwkSet(snapshot))
36222
36016
  throw new JWKSInvalid("JSON Web Key Set malformed");
36223
- }
36224
- const cached3 = /* @__PURE__ */ new WeakMap();
36225
- const localJWKSet = async (protectedHeader, token) => {
36226
- const { alg, kid } = { ...protectedHeader, ...token?.header };
36227
- const entry = typeof alg === "string" ? JWS[alg] : void 0;
36228
- if (!entry || entry.secret) {
36017
+ const metadata = snapshot.keys.map((jwk) => {
36018
+ const normalized = snapshotJwk(jwk);
36019
+ return Array.isArray(normalized.key_ops) && (normalized.key_ops = [...normalized.key_ops]), normalized;
36020
+ }), cached2 = /* @__PURE__ */ new WeakMap();
36021
+ return Object.defineProperty(async (protectedHeader, token) => {
36022
+ const { alg, kid } = { ...protectedHeader, ...token?.header }, entry = typeof alg == "string" ? JWS[alg] : void 0;
36023
+ if (!entry || entry.secret)
36229
36024
  throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set');
36230
- }
36231
- const candidates = snapshot.keys.filter((jwk2) => isUsableJWK(jwk2, entry, alg, kid));
36232
- const { 0: jwk, length } = candidates;
36233
- if (!length) {
36025
+ const candidates = snapshot.keys.filter((_, index) => isUsableJWK(metadata[index], entry, alg, kid)), { 0: jwk, length } = candidates;
36026
+ if (!length)
36234
36027
  throw new JWKSNoMatchingKey();
36235
- }
36236
36028
  if (length !== 1) {
36237
36029
  const error2 = new JWKSMultipleMatchingKeys();
36238
- error2[Symbol.asyncIterator] = async function* () {
36239
- for (const jwk2 of candidates) {
36030
+ throw error2[Symbol.asyncIterator] = async function* () {
36031
+ for (const jwk2 of candidates)
36240
36032
  try {
36241
- yield await importWithAlgCache(cached3, jwk2, entry);
36033
+ yield await importWithAlgCache(cached2, jwk2, entry);
36242
36034
  } catch {
36243
36035
  }
36244
- }
36245
- };
36246
- throw error2;
36036
+ }, error2;
36247
36037
  }
36248
- return importWithAlgCache(cached3, jwk, entry);
36249
- };
36250
- return Object.defineProperty(localJWKSet, "jwks", {
36038
+ return importWithAlgCache(cached2, jwk, entry);
36039
+ }, "jwks", {
36251
36040
  value: () => structuredClone(snapshot)
36252
36041
  });
36253
36042
  }
36254
36043
 
36255
36044
  // node_modules/jose/dist/webapi/jwks/remote.js
36256
36045
  function isCloudflareWorkers() {
36257
- return typeof WebSocketPair !== "undefined" || typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime !== "undefined" && EdgeRuntime === "vercel";
36046
+ return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel";
36258
36047
  }
36259
36048
  var USER_AGENT;
36260
- if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) {
36261
- const NAME = "jose";
36262
- const VERSION2 = "v6.2.10";
36263
- USER_AGENT = `${NAME}/${VERSION2}`;
36264
- }
36265
- var customFetch = Symbol();
36049
+ (typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12");
36050
+ var customFetch = /* @__PURE__ */ Symbol();
36266
36051
  async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
36267
36052
  const response = await fetchImpl(url, {
36268
36053
  method: "GET",
@@ -36270,106 +36055,64 @@ async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
36270
36055
  redirect: "manual",
36271
36056
  headers
36272
36057
  }).catch((err) => {
36273
- if (err.name === "TimeoutError") {
36274
- throw new JWKSTimeout();
36275
- }
36276
- throw err;
36058
+ throw err.name === "TimeoutError" ? new JWKSTimeout() : err;
36277
36059
  });
36278
- if (response.status !== 200) {
36060
+ if (response.status !== 200)
36279
36061
  throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response");
36280
- }
36281
36062
  try {
36282
36063
  return await response.json();
36283
36064
  } catch {
36284
36065
  throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON");
36285
36066
  }
36286
36067
  }
36287
- var jwksCache = Symbol();
36068
+ var jwksCache = /* @__PURE__ */ Symbol();
36288
36069
  function isFreshFor(timestamp, duration3) {
36289
36070
  return Number.isFinite(timestamp) && Date.now() < timestamp + duration3;
36290
36071
  }
36291
36072
  function validateDuration(value, fallback, option) {
36292
- if (Number.isNaN(value)) {
36073
+ if (Number.isNaN(value))
36293
36074
  throw new TypeError(`"${option}" option must not be NaN`);
36294
- }
36295
- return typeof value === "number" ? value : fallback;
36075
+ return typeof value == "number" ? value : fallback;
36296
36076
  }
36297
36077
  function createRemoteJWKSet(url, options) {
36298
- if (!(url instanceof URL)) {
36078
+ if (!(url instanceof URL))
36299
36079
  throw new TypeError("url must be an instance of URL");
36300
- }
36301
- const href = new URL(url.href).href;
36302
- const opts = options ?? {};
36303
- const timeoutOption = opts.timeoutDuration;
36304
- if (typeof timeoutOption === "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0)) {
36080
+ const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration;
36081
+ if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0))
36305
36082
  throw new TypeError('"timeoutDuration" option must be a non-negative integer');
36306
- }
36307
- const timeoutDuration = typeof timeoutOption === "number" ? timeoutOption : 5e3;
36308
- const cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration");
36309
- const cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge");
36310
- const headers = new Headers(opts.headers);
36311
- if (USER_AGENT && !headers.has("User-Agent")) {
36312
- headers.set("User-Agent", USER_AGENT);
36313
- }
36314
- if (!headers.has("accept")) {
36315
- headers.set("accept", "application/json, application/jwk-set+json");
36316
- }
36317
- const fetchImpl = opts[customFetch];
36318
- const cache2 = opts[jwksCache];
36319
- let jwksTimestamp;
36320
- let pendingFetch;
36321
- let reloadSequence = 0;
36322
- let appliedSequence = 0;
36323
- let local;
36324
- if (cache2 && typeof cache2 === "object") {
36083
+ const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers);
36084
+ USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json");
36085
+ const fetchImpl = opts[customFetch], cache2 = opts[jwksCache];
36086
+ let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local;
36087
+ if (cache2 && typeof cache2 == "object") {
36325
36088
  const { uat, jwks } = cache2;
36326
- if (isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks)) {
36327
- jwksTimestamp = uat;
36328
- local = createLocalJWKSet(jwks);
36329
- }
36089
+ isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks));
36330
36090
  }
36331
36091
  const reload = async () => {
36332
- if (pendingFetch && isCloudflareWorkers()) {
36333
- pendingFetch = void 0;
36334
- }
36335
- if (!pendingFetch) {
36336
- const sequence = ++reloadSequence;
36337
- const current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => {
36092
+ if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) {
36093
+ const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => {
36338
36094
  const next = createLocalJWKSet(json);
36339
- if (sequence <= appliedSequence) {
36095
+ if (sequence <= appliedSequence)
36340
36096
  return;
36341
- }
36342
36097
  local = next;
36343
36098
  const updatedAt = Date.now();
36344
- if (cache2) {
36345
- cache2.uat = updatedAt;
36346
- cache2.jwks = json;
36347
- }
36348
- jwksTimestamp = updatedAt;
36349
- appliedSequence = sequence;
36099
+ cache2 && (cache2.uat = updatedAt, cache2.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence;
36350
36100
  }).finally(() => {
36351
- if (pendingFetch === current) {
36352
- pendingFetch = void 0;
36353
- }
36101
+ pendingFetch === current && (pendingFetch = void 0);
36354
36102
  });
36355
36103
  }
36356
36104
  await pendingFetch;
36357
36105
  };
36358
- const remoteJWKSet = async (protectedHeader, token) => {
36359
- if (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) {
36360
- await reload();
36361
- }
36106
+ return Object.defineProperties(async (protectedHeader, token) => {
36107
+ (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload();
36362
36108
  try {
36363
36109
  return await local(protectedHeader, token);
36364
36110
  } catch (err) {
36365
- if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) {
36366
- await reload();
36367
- return local(protectedHeader, token);
36368
- }
36111
+ if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration))
36112
+ return await reload(), local(protectedHeader, token);
36369
36113
  throw err;
36370
36114
  }
36371
- };
36372
- return Object.defineProperties(remoteJWKSet, {
36115
+ }, {
36373
36116
  coolingDown: {
36374
36117
  get: () => isFreshFor(jwksTimestamp, cooldownDuration),
36375
36118
  enumerable: true
@@ -36472,16 +36215,16 @@ var JwtAuthProvider = class _JwtAuthProvider extends AuthProvider {
36472
36215
  }
36473
36216
  async verifyToken(token) {
36474
36217
  const cacheKey2 = await this.hashToken(token);
36475
- const cached3 = this.tokenCache.get(cacheKey2);
36476
- if (cached3 && Date.now() < cached3.expiresAt) {
36218
+ const cached2 = this.tokenCache.get(cacheKey2);
36219
+ if (cached2 && Date.now() < cached2.expiresAt) {
36477
36220
  if (isOtelEnabled()) {
36478
36221
  recordAuthEvent("cache_hit", {
36479
- subject: cached3.authInfo.subject ?? ""
36222
+ subject: cached2.authInfo.subject ?? ""
36480
36223
  });
36481
36224
  }
36482
- return cached3.authInfo;
36225
+ return cached2.authInfo;
36483
36226
  }
36484
- if (cached3) this.tokenCache.delete(cacheKey2);
36227
+ if (cached2) this.tokenCache.delete(cacheKey2);
36485
36228
  try {
36486
36229
  const { payload } = await jwtVerify(token, this.jwks, {
36487
36230
  issuer: this.options.issuer,
@@ -41669,6 +41412,13 @@ var createContextStore3 = impl2.createContextStore;
41669
41412
  // src/cache/memory.ts
41670
41413
  var MemoryCache = class {
41671
41414
  store = /* @__PURE__ */ new Map();
41415
+ maxEntries;
41416
+ constructor(maxEntries = 1e3) {
41417
+ if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
41418
+ throw new RangeError("maxEntries must be a positive finite integer");
41419
+ }
41420
+ this.maxEntries = maxEntries;
41421
+ }
41672
41422
  get(key) {
41673
41423
  const entry = this.store.get(key);
41674
41424
  if (!entry) return void 0;
@@ -41679,7 +41429,21 @@ var MemoryCache = class {
41679
41429
  return structuredClone(entry.value);
41680
41430
  }
41681
41431
  set(key, value, ttlMs) {
41682
- this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
41432
+ const now = Date.now();
41433
+ for (const [storedKey, entry] of this.store) {
41434
+ if (now >= entry.expiresAt) this.store.delete(storedKey);
41435
+ }
41436
+ if (ttlMs <= 0) {
41437
+ this.store.delete(key);
41438
+ return;
41439
+ }
41440
+ const snapshot = structuredClone(value);
41441
+ this.store.delete(key);
41442
+ if (this.store.size >= this.maxEntries) {
41443
+ const oldest = this.store.keys().next();
41444
+ if (!oldest.done) this.store.delete(oldest.value);
41445
+ }
41446
+ this.store.set(key, { value: snapshot, expiresAt: now + ttlMs });
41683
41447
  }
41684
41448
  delete(key) {
41685
41449
  this.store.delete(key);
@@ -41734,6 +41498,20 @@ function getCache() {
41734
41498
  return _cache;
41735
41499
  }
41736
41500
 
41501
+ // src/cache/invalidation-generation.ts
41502
+ var generations = /* @__PURE__ */ new WeakMap();
41503
+ function getInvalidationGeneration(cache2, doctype) {
41504
+ return generations.get(cache2)?.get(doctype);
41505
+ }
41506
+ function bumpInvalidationGeneration(cache2, doctype) {
41507
+ let byDoctype = generations.get(cache2);
41508
+ if (byDoctype === void 0) {
41509
+ byDoctype = /* @__PURE__ */ new Map();
41510
+ generations.set(cache2, byDoctype);
41511
+ }
41512
+ byDoctype.set(doctype, Symbol());
41513
+ }
41514
+
41737
41515
  // src/api/caller-context.ts
41738
41516
  var callerStorage = createContextStore3();
41739
41517
  function runWithCaller(identity, fn) {
@@ -41941,16 +41719,47 @@ var FrappeClient = class {
41941
41719
  const url = `${this.baseUrl}${path}`;
41942
41720
  const controller = new AbortController();
41943
41721
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
41944
- let response;
41945
41722
  try {
41946
- response = await fetch(url, {
41723
+ const response = await fetch(url, {
41947
41724
  method,
41948
41725
  headers: this.buildHeaders(!multipart),
41949
41726
  body: body === void 0 ? void 0 : multipart ? body : JSON.stringify(body),
41950
41727
  signal: controller.signal
41951
41728
  });
41729
+ const rawText = await response.text();
41730
+ const contentType = response.headers.get("content-type") ?? "";
41731
+ let responseBody = rawText;
41732
+ if (contentType.includes("application/json") && rawText.length > 0) {
41733
+ try {
41734
+ responseBody = JSON.parse(rawText);
41735
+ } catch {
41736
+ responseBody = rawText;
41737
+ }
41738
+ }
41739
+ if (!response.ok) {
41740
+ let msg = response.statusText;
41741
+ if (typeof responseBody === "object" && responseBody !== null) {
41742
+ const rb = responseBody;
41743
+ const excType = rb.exc_type;
41744
+ const baseMsg = rb.message ?? excType ?? response.statusText;
41745
+ const serverDetails = extractServerMessages(rb._server_messages);
41746
+ msg = serverDetails ? `${baseMsg}: ${serverDetails}` : baseMsg;
41747
+ } else if (typeof responseBody === "string" && responseBody.length > 0) {
41748
+ msg = responseBody.slice(0, 200);
41749
+ }
41750
+ const retryAfterMs = parseRetryAfter(
41751
+ response.headers.get("retry-after")
41752
+ );
41753
+ throw new FrappeAPIError(
41754
+ `${method} ${path} failed: ${msg}`,
41755
+ response.status,
41756
+ responseBody,
41757
+ retryAfterMs
41758
+ );
41759
+ }
41760
+ return responseBody;
41952
41761
  } catch (err) {
41953
- clearTimeout(timer);
41762
+ if (err instanceof FrappeAPIError) throw err;
41954
41763
  if (err instanceof Error && err.name === "AbortError") {
41955
41764
  throw new FrappeAPIError(
41956
41765
  `Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
@@ -41963,40 +41772,9 @@ var FrappeClient = class {
41963
41772
  0,
41964
41773
  null
41965
41774
  );
41775
+ } finally {
41776
+ clearTimeout(timer);
41966
41777
  }
41967
- clearTimeout(timer);
41968
- const rawText = await response.text();
41969
- const contentType = response.headers.get("content-type") ?? "";
41970
- let responseBody = rawText;
41971
- if (contentType.includes("application/json") && rawText.length > 0) {
41972
- try {
41973
- responseBody = JSON.parse(rawText);
41974
- } catch {
41975
- responseBody = rawText;
41976
- }
41977
- }
41978
- if (!response.ok) {
41979
- let msg = response.statusText;
41980
- if (typeof responseBody === "object" && responseBody !== null) {
41981
- const rb = responseBody;
41982
- const excType = rb.exc_type;
41983
- const baseMsg = rb.message ?? excType ?? response.statusText;
41984
- const serverDetails = extractServerMessages(rb._server_messages);
41985
- msg = serverDetails ? `${baseMsg}: ${serverDetails}` : baseMsg;
41986
- } else if (typeof responseBody === "string" && responseBody.length > 0) {
41987
- msg = responseBody.slice(0, 200);
41988
- }
41989
- const retryAfterMs = parseRetryAfter(
41990
- response.headers.get("retry-after")
41991
- );
41992
- throw new FrappeAPIError(
41993
- `${method} ${path} failed: ${msg}`,
41994
- response.status,
41995
- responseBody,
41996
- retryAfterMs
41997
- );
41998
- }
41999
- return responseBody;
42000
41778
  }
42001
41779
  // ── Resource CRUD ───────────────────────────────────────────────────────────
42002
41780
  /**
@@ -42015,8 +41793,8 @@ var FrappeClient = class {
42015
41793
  };
42016
41794
  const cacheKey2 = `list:${doctype}:${stableStringify(options)}`;
42017
41795
  if (!opts.skipCache) {
42018
- const cached3 = this.cache.get(cacheKey2);
42019
- if (cached3 !== void 0) return cached3;
41796
+ const cached2 = this.cache.get(cacheKey2);
41797
+ if (cached2 !== void 0) return cached2;
42020
41798
  }
42021
41799
  const params = new URLSearchParams();
42022
41800
  if (options.fields && options.fields.length > 0) {
@@ -42036,12 +41814,15 @@ var FrappeClient = class {
42036
41814
  }
42037
41815
  params.set("as_dict", "1");
42038
41816
  const query = params.toString() ? `?${params.toString()}` : "";
41817
+ const generation = getInvalidationGeneration(this.cache, doctype);
42039
41818
  const res = await this.request(
42040
41819
  "GET",
42041
41820
  `/api/resource/${encodeURIComponent(doctype)}${query}`
42042
41821
  );
42043
41822
  const docs = res.data ?? [];
42044
- this.cache.set(cacheKey2, docs, getCacheTtlMs());
41823
+ if (generation === getInvalidationGeneration(this.cache, doctype)) {
41824
+ this.cache.set(cacheKey2, docs, getCacheTtlMs());
41825
+ }
42045
41826
  return docs;
42046
41827
  }
42047
41828
  /**
@@ -42056,14 +41837,17 @@ var FrappeClient = class {
42056
41837
  async get(doctype, name, opts = {}) {
42057
41838
  const cacheKey2 = `get:${doctype}:${name}`;
42058
41839
  if (!opts.skipCache) {
42059
- const cached3 = this.cache.get(cacheKey2);
42060
- if (cached3 !== void 0) return cached3;
41840
+ const cached2 = this.cache.get(cacheKey2);
41841
+ if (cached2 !== void 0) return cached2;
42061
41842
  }
41843
+ const generation = getInvalidationGeneration(this.cache, doctype);
42062
41844
  const res = await this.request(
42063
41845
  "GET",
42064
41846
  `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
42065
41847
  );
42066
- this.cache.set(cacheKey2, res.data, getCacheTtlMs());
41848
+ if (generation === getInvalidationGeneration(this.cache, doctype)) {
41849
+ this.cache.set(cacheKey2, res.data, getCacheTtlMs());
41850
+ }
42067
41851
  return res.data;
42068
41852
  }
42069
41853
  /**
@@ -42083,6 +41867,7 @@ var FrappeClient = class {
42083
41867
  */
42084
41868
  invalidate(doctype, name) {
42085
41869
  const clear = (cache2) => {
41870
+ bumpInvalidationGeneration(cache2, doctype);
42086
41871
  cache2.deleteByPrefix(`list:${doctype}:`);
42087
41872
  cache2.deleteByPrefix(`resolve:miss:${doctype}:`);
42088
41873
  if (name) cache2.delete(`get:${doctype}:${name}`);
@@ -42350,8 +42135,8 @@ async function currentUserId(client) {
42350
42135
  async function loadCallerProfile(client) {
42351
42136
  const key = cacheKey(client);
42352
42137
  const now = Date.now();
42353
- for (const [cached3, entry] of profileCache) {
42354
- if (entry.expiresAt <= now) profileCache.delete(cached3);
42138
+ for (const [cached2, entry] of profileCache) {
42139
+ if (entry.expiresAt <= now) profileCache.delete(cached2);
42355
42140
  }
42356
42141
  const hit = profileCache.get(key);
42357
42142
  if (hit && hit.expiresAt > now) return hit.profile;
@@ -42707,6 +42492,7 @@ async function applyAssignment(doctype, name, assignment, ctx, failureContext) {
42707
42492
  const reason = error2 instanceof Error ? error2.message : String(error2);
42708
42493
  throw new Error(`${failureContext}: ${reason}`, { cause: error2 });
42709
42494
  }
42495
+ invalidateAssignment(doctype, name, nativeResult, ctx);
42710
42496
  const todos = Array.isArray(nativeResult) ? nativeResult.map((todo) => {
42711
42497
  const record2 = todo;
42712
42498
  return { owner: record2.owner, name: record2.name };
@@ -42725,15 +42511,26 @@ async function removeAssignment(doctype, name, assignee, ctx, failureContext) {
42725
42511
  const reason = error2 instanceof Error ? error2.message : String(error2);
42726
42512
  throw new Error(`${failureContext}: ${reason}`, { cause: error2 });
42727
42513
  }
42514
+ invalidateAssignment(doctype, name, nativeResult, ctx);
42728
42515
  const todos = Array.isArray(nativeResult) ? nativeResult.map((todo) => {
42729
42516
  const record2 = todo;
42730
42517
  return { owner: record2.owner, name: record2.name };
42731
42518
  }) : [];
42732
42519
  return { removed: assignee, remaining: todos };
42733
42520
  }
42521
+ function invalidateAssignment(doctype, name, nativeResult, ctx) {
42522
+ ctx.client.invalidate(doctype, name);
42523
+ ctx.client.invalidate("ToDo");
42524
+ if (!Array.isArray(nativeResult)) return;
42525
+ for (const todo of nativeResult) {
42526
+ if (typeof todo === "object" && todo !== null && typeof todo.name === "string" && todo.name.trim()) {
42527
+ ctx.client.invalidate("ToDo", todo.name);
42528
+ }
42529
+ }
42530
+ }
42734
42531
  async function fetchDocAfterAssignment(doctype, name, ctx, toolName, action = "assignment") {
42735
42532
  try {
42736
- return await ctx.client.get(doctype, name);
42533
+ return await ctx.client.get(doctype, name, { skipCache: true });
42737
42534
  } catch (error2) {
42738
42535
  const reason = error2 instanceof Error ? error2.message : String(error2);
42739
42536
  throw new Error(
@@ -44141,6 +43938,76 @@ var salesTools = [
44141
43938
 
44142
43939
  // src/tools/inventory.ts
44143
43940
  var inventoryTools = [
43941
+ {
43942
+ name: "erpnext_stock_ledger_list",
43943
+ annotations: { readOnlyHint: true },
43944
+ description: "List recent non-cancelled Stock Ledger Entries for one item and warehouse. Item accepts an ID or name; warehouse must be its exact ID. Returns {data: rows}, newest posting date/time/name first, with voucher and quantity fields.",
43945
+ category: "inventory",
43946
+ inputSchema: {
43947
+ type: "object",
43948
+ additionalProperties: false,
43949
+ required: ["item_code", "warehouse"],
43950
+ properties: {
43951
+ item_code: {
43952
+ type: "string",
43953
+ minLength: 1,
43954
+ pattern: "\\S",
43955
+ description: "Item ID or name"
43956
+ },
43957
+ warehouse: {
43958
+ type: "string",
43959
+ minLength: 1,
43960
+ pattern: "\\S",
43961
+ description: "Exact warehouse ID"
43962
+ },
43963
+ limit: {
43964
+ type: "integer",
43965
+ minimum: 1,
43966
+ maximum: 20,
43967
+ default: 5,
43968
+ description: "Max results (default 5, maximum 20)"
43969
+ }
43970
+ }
43971
+ },
43972
+ handler: async (input, ctx) => {
43973
+ for (const field of ["item_code", "warehouse"]) {
43974
+ if (typeof input[field] !== "string" || !input[field].trim()) {
43975
+ throw new Error(`'${field}' must be a non-empty string`);
43976
+ }
43977
+ }
43978
+ const limit = input.limit === void 0 ? 5 : input.limit;
43979
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 20) {
43980
+ throw new Error("'limit' must be an integer between 1 and 20");
43981
+ }
43982
+ const itemCode = await resolveItem(
43983
+ ctx.client,
43984
+ input.item_code,
43985
+ { inputPath: "item_code" }
43986
+ );
43987
+ const rows = await ctx.client.list("Stock Ledger Entry", {
43988
+ fields: [
43989
+ "name",
43990
+ "item_code",
43991
+ "warehouse",
43992
+ "posting_date",
43993
+ "posting_time",
43994
+ "voucher_type",
43995
+ "voucher_no",
43996
+ "actual_qty",
43997
+ "qty_after_transaction",
43998
+ "stock_uom"
43999
+ ],
44000
+ filters: [["item_code", "=", itemCode], [
44001
+ "warehouse",
44002
+ "=",
44003
+ input.warehouse
44004
+ ], ["is_cancelled", "=", 0]],
44005
+ order_by: "posting_date desc, posting_time desc, name desc",
44006
+ limit
44007
+ }, { skipCache: true });
44008
+ return { data: rows };
44009
+ }
44010
+ },
44144
44011
  // ── Items ─────────────────────────────────────────────────────────────────
44145
44012
  {
44146
44013
  name: "erpnext_item_list",
@@ -44596,6 +44463,63 @@ var inventoryTools = [
44596
44463
  }
44597
44464
  ];
44598
44465
 
44466
+ // src/tools/analytics-pagination.ts
44467
+ var ANALYTICS_PAGE_SIZE = 1e3;
44468
+ var ANALYTICS_MAX_ROWS = 1e5;
44469
+ var ANALYTICS_MAX_REQUESTS = 1e3;
44470
+ function completePageOptions(options, offset) {
44471
+ const order2 = options.order_by ?? "modified desc";
44472
+ if (!/^[a-z_]+ (?:asc|desc)(?:,\s*[a-z_]+ (?:asc|desc))*$/.test(order2)) {
44473
+ throw new Error("Analytics pagination requires explicit field ordering.");
44474
+ }
44475
+ return {
44476
+ ...options,
44477
+ fields: [.../* @__PURE__ */ new Set([...options.fields ?? [], "name"])],
44478
+ order_by: order2.split(",").some((part) => part.trim().startsWith("name ")) ? order2 : `${order2}, name asc`,
44479
+ limit: ANALYTICS_PAGE_SIZE,
44480
+ limit_start: offset
44481
+ };
44482
+ }
44483
+ async function listCompleteAnalytics(list, doctype, options, budget = { requests: 0, rows: 0 }) {
44484
+ const result = [];
44485
+ const names = /* @__PURE__ */ new Set();
44486
+ for (let offset = 0; ; offset += ANALYTICS_PAGE_SIZE) {
44487
+ if (budget.requests >= ANALYTICS_MAX_REQUESTS) {
44488
+ throw new Error(
44489
+ `Analytics ${doctype} cannot prove completeness within the ${ANALYTICS_MAX_REQUESTS} request safety limit.`
44490
+ );
44491
+ }
44492
+ budget.requests++;
44493
+ const rows = await list(doctype, completePageOptions(options, offset));
44494
+ if (!Array.isArray(rows) || rows.length > ANALYTICS_PAGE_SIZE) {
44495
+ throw new Error(
44496
+ `Analytics ${doctype} returned an invalid or oversized page.`
44497
+ );
44498
+ }
44499
+ budget.rows += rows.length;
44500
+ if (budget.rows > ANALYTICS_MAX_ROWS) {
44501
+ throw new Error(
44502
+ `Analytics ${doctype} cannot prove completeness within the ${ANALYTICS_MAX_ROWS} row safety limit.`
44503
+ );
44504
+ }
44505
+ for (const row of rows) {
44506
+ if (!row || typeof row !== "object" || Array.isArray(row) || typeof row.name !== "string" || row.name.trim() === "") {
44507
+ throw new Error(
44508
+ `Analytics ${doctype} returned a row without a valid name.`
44509
+ );
44510
+ }
44511
+ if (names.has(row.name)) {
44512
+ throw new Error(
44513
+ `Analytics ${doctype} pagination did not make unique progress; data may have changed during the read.`
44514
+ );
44515
+ }
44516
+ names.add(row.name);
44517
+ result.push(row);
44518
+ }
44519
+ if (rows.length < ANALYTICS_PAGE_SIZE) return result;
44520
+ }
44521
+ }
44522
+
44599
44523
  // src/tools/query-report.ts
44600
44524
  var FINANCIAL_REPORTS = [
44601
44525
  "Accounts Payable",
@@ -44663,6 +44587,85 @@ function cellNumber(value) {
44663
44587
  }
44664
44588
  return null;
44665
44589
  }
44590
+ async function receivableInvoiceRows(ctx, company, currency, reportDate) {
44591
+ const report = await runQueryReport(ctx, "Accounts Receivable", {
44592
+ company,
44593
+ report_date: reportDate,
44594
+ in_party_currency: 0,
44595
+ based_on_payment_terms: 0,
44596
+ group_by_party: 0
44597
+ });
44598
+ if (report.result.length > ANALYTICS_MAX_ROWS) {
44599
+ throw new Error(
44600
+ `Accounts Receivable exceeds the ${ANALYTICS_MAX_ROWS} row safety limit; no partial balance will be reported.`
44601
+ );
44602
+ }
44603
+ const invoices = [];
44604
+ const voucherKeys = /* @__PURE__ */ new Set();
44605
+ for (const row of report.result) {
44606
+ if (row === null || typeof row !== "object" || Array.isArray(row)) {
44607
+ throw new Error("Accounts Receivable returned an invalid row.");
44608
+ }
44609
+ if (row.voucher_type !== "Sales Invoice") continue;
44610
+ if (typeof row.voucher_no !== "string" || row.voucher_no === "") {
44611
+ throw new Error(
44612
+ "Accounts Receivable returned a Sales Invoice without voucher_no."
44613
+ );
44614
+ }
44615
+ if (typeof row.party_account !== "string" || row.party_account === "" || typeof row.party !== "string" || row.party === "") {
44616
+ throw new Error(
44617
+ `Accounts Receivable invoice '${row.voucher_no}' has no ledger ownership key.`
44618
+ );
44619
+ }
44620
+ const key = JSON.stringify([
44621
+ row.party_account,
44622
+ row.voucher_type,
44623
+ row.voucher_no,
44624
+ row.party
44625
+ ]);
44626
+ if (voucherKeys.has(key)) {
44627
+ throw new Error(
44628
+ `Accounts Receivable returned a duplicate ledger balance for invoice '${row.voucher_no}'.`
44629
+ );
44630
+ }
44631
+ voucherKeys.add(key);
44632
+ if (row.currency !== currency) {
44633
+ throw new Error(
44634
+ `Accounts Receivable invoice '${row.voucher_no}' has missing or unexpected currency; expected ${currency}.`
44635
+ );
44636
+ }
44637
+ const amount = cellNumber(row.outstanding);
44638
+ if (amount === null) {
44639
+ throw new Error(
44640
+ `Accounts Receivable invoice '${row.voucher_no}' has an invalid outstanding amount.`
44641
+ );
44642
+ }
44643
+ if (amount <= 0) continue;
44644
+ const postingDate = row.posting_date;
44645
+ const dueDate = row.due_date || postingDate;
44646
+ for (const value of [postingDate, dueDate]) {
44647
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value))) {
44648
+ throw new Error(
44649
+ `Accounts Receivable invoice '${row.voucher_no}' has an invalid date.`
44650
+ );
44651
+ }
44652
+ }
44653
+ const name = row.customer_name || row.party_name || row.party;
44654
+ if (typeof name !== "string" || name === "") {
44655
+ throw new Error(
44656
+ `Accounts Receivable invoice '${row.voucher_no}' has no customer identity.`
44657
+ );
44658
+ }
44659
+ invoices.push({
44660
+ voucher_no: row.voucher_no,
44661
+ customer_name: name,
44662
+ outstanding_amount: amount,
44663
+ posting_date: postingDate,
44664
+ due_date: dueDate
44665
+ });
44666
+ }
44667
+ return invoices;
44668
+ }
44666
44669
 
44667
44670
  // src/tools/accounting.ts
44668
44671
  var DEFAULT_REPORT_ROWS = 100;
@@ -45187,14 +45190,14 @@ var accountingTools = [
45187
45190
  ];
45188
45191
 
45189
45192
  // src/tools/site-date.ts
45190
- async function siteTimeZone(ctx) {
45193
+ async function resolveTimeZone(ctx) {
45191
45194
  try {
45192
45195
  const value = await ctx.client.callMethod(
45193
45196
  "frappe.client.get_value",
45194
45197
  { doctype: "System Settings", fieldname: "time_zone" },
45195
45198
  { httpMethod: "GET" }
45196
45199
  );
45197
- if (value?.time_zone) return value.time_zone;
45200
+ if (value?.time_zone) return { zone: value.time_zone, authoritative: true };
45198
45201
  } catch {
45199
45202
  }
45200
45203
  try {
@@ -45203,29 +45206,105 @@ async function siteTimeZone(ctx) {
45203
45206
  {},
45204
45207
  { httpMethod: "GET" }
45205
45208
  );
45206
- if (value?.time_zone) return value.time_zone;
45209
+ if (value?.time_zone) {
45210
+ return { zone: value.time_zone, authoritative: false };
45211
+ }
45207
45212
  } catch {
45208
45213
  }
45209
- return null;
45214
+ return { zone: null, authoritative: false };
45210
45215
  }
45211
- async function siteToday(ctx) {
45212
- const timeZone = await siteTimeZone(ctx);
45216
+ async function siteNow(ctx) {
45217
+ const { zone, authoritative } = await resolveTimeZone(ctx);
45213
45218
  const now = /* @__PURE__ */ new Date();
45214
- if (timeZone) {
45219
+ if (zone) {
45215
45220
  try {
45216
- return new Intl.DateTimeFormat("en-CA", {
45217
- timeZone,
45221
+ const formatted = new Intl.DateTimeFormat("en-CA", {
45222
+ timeZone: zone,
45218
45223
  year: "numeric",
45219
45224
  month: "2-digit",
45220
- day: "2-digit"
45221
- }).format(now);
45225
+ day: "2-digit",
45226
+ hour: "2-digit",
45227
+ minute: "2-digit",
45228
+ second: "2-digit",
45229
+ // `h23` chứ không phải `hour12: false`: nhánh 12 giờ tắt vẫn cho ra "24:00:00" ở
45230
+ // nửa đêm trên một số runtime, và "24" không phải giờ hợp lệ của một chuỗi Datetime.
45231
+ hourCycle: "h23"
45232
+ }).format(now).replace(", ", " ");
45233
+ return { now: formatted, authoritative };
45222
45234
  } catch {
45223
45235
  }
45224
45236
  }
45225
- return now.toISOString().slice(0, 10);
45237
+ return {
45238
+ now: now.toISOString().slice(0, 19).replace("T", " "),
45239
+ authoritative: false
45240
+ };
45241
+ }
45242
+ async function siteToday(ctx) {
45243
+ return (await siteNow(ctx)).now.slice(0, 10);
45226
45244
  }
45227
45245
 
45228
45246
  // src/tools/hr.ts
45247
+ var MIN_FIX_REASON = 10;
45248
+ var REQUIRES_HVG_WORKSPACE = "Requires the 'hvg_workspace' app on the site; without it the call fails and the generic erpnext_doc_* tools are the only route.";
45249
+ var METHOD_LOOKUP_FAILURE = /Failed to get method|Method Not Found/i;
45250
+ async function callHvgWorkspace(ctx, method, args, tool) {
45251
+ try {
45252
+ return await ctx.client.callMethod(method, args);
45253
+ } catch (err) {
45254
+ const text = err instanceof Error ? err.message : String(err);
45255
+ const body = err instanceof FrappeAPIError && err.body !== null ? JSON.stringify(err.body) : "";
45256
+ if (METHOD_LOOKUP_FAILURE.test(text) || METHOD_LOOKUP_FAILURE.test(body)) {
45257
+ throw new Error(
45258
+ `[${tool}] this site does not expose '${method}'. These attendance repair tools need the 'hvg_workspace' app installed. Original error: ` + text,
45259
+ { cause: err }
45260
+ );
45261
+ }
45262
+ throw err;
45263
+ }
45264
+ }
45265
+ async function readAttendanceDay(ctx, employee, date3, tool) {
45266
+ return await callHvgWorkspace(
45267
+ ctx,
45268
+ "hvg_workspace.api.hr_get_day_attendance",
45269
+ { employee, date: date3 },
45270
+ tool
45271
+ );
45272
+ }
45273
+ function nextDayISO(date3, tool) {
45274
+ const parsed = /* @__PURE__ */ new Date(`${date3}T00:00:00Z`);
45275
+ const roundTrip = Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
45276
+ if (roundTrip !== date3) {
45277
+ throw new Error(
45278
+ `[${tool}] '${date3}' is not a real calendar date. Use YYYY-MM-DD.`
45279
+ );
45280
+ }
45281
+ parsed.setUTCDate(parsed.getUTCDate() + 1);
45282
+ return parsed.toISOString().slice(0, 10);
45283
+ }
45284
+ function normalizeStamp(time3) {
45285
+ return time3.replace("T", " ").slice(0, 19);
45286
+ }
45287
+ function assertLogType(value, tool) {
45288
+ const text = String(value ?? "").toUpperCase();
45289
+ if (text !== "IN" && text !== "OUT") {
45290
+ throw new Error(
45291
+ `[${tool}] log_type must be 'IN' or 'OUT', got ${JSON.stringify(value)}.`
45292
+ );
45293
+ }
45294
+ }
45295
+ function orderedPunches(target, tool) {
45296
+ const blank = target.filter((row) => !row.log_type);
45297
+ if (blank.length > 0) {
45298
+ throw new Error(
45299
+ `[${tool}] these punches carry no log_type in ERPNext: ` + blank.map((row) => `${row.name} (${row.time})`).join(", ") + ". The site refuses a save with a blank direction, and this tool does not guess one. Name each of them in 'edit' with the log_type it should carry."
45300
+ );
45301
+ }
45302
+ return [...target].sort((a, b) => a.time.localeCompare(b.time)).map((row) => ({
45303
+ name: row.name,
45304
+ time: row.time,
45305
+ log_type: row.log_type
45306
+ }));
45307
+ }
45229
45308
  var hrTools = [
45230
45309
  // ── Employees ─────────────────────────────────────────────────────────────
45231
45310
  {
@@ -45405,6 +45484,345 @@ var hrTools = [
45405
45484
  });
45406
45485
  }
45407
45486
  },
45487
+ // ── Employee Checkins & day repair ────────────────────────────────────────
45488
+ {
45489
+ name: "erpnext_employee_checkin_list",
45490
+ annotations: { readOnlyHint: true },
45491
+ _meta: DOCLIST_META,
45492
+ description: "List Employee Checkin punches for an employee over a date range. This is the raw punch log behind Attendance, so it is where a broken day shows up: a day with an odd number of punches never closed, and a punch whose 'attendance' is empty was never counted. 'skip_auto_attendance' = 1 means auto attendance already refused this punch, which is what happens when a checkin is added to a day that already carries an Attendance record. Fields: name, employee, employee_name, time, log_type, shift, attendance, skip_auto_attendance. Use erpnext_attendance_day_get for the authoritative per-day view, and erpnext_attendance_day_fix to repair a day.",
45493
+ category: "hr",
45494
+ inputSchema: {
45495
+ type: "object",
45496
+ properties: {
45497
+ limit: {
45498
+ type: "number",
45499
+ minimum: 1,
45500
+ description: "Max results (default 50)"
45501
+ },
45502
+ employee: {
45503
+ type: "string",
45504
+ description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
45505
+ },
45506
+ log_type: {
45507
+ type: "string",
45508
+ description: "Filter by direction. Devices that do not report a direction leave this empty, so filtering on it can hide real punches.",
45509
+ enum: ["IN", "OUT"]
45510
+ },
45511
+ date_from: {
45512
+ type: "string",
45513
+ description: "Start date filter YYYY-MM-DD (time >=, from 00:00:00)"
45514
+ },
45515
+ date_to: {
45516
+ type: "string",
45517
+ description: "End date filter YYYY-MM-DD (time <=, up to 23:59:59)"
45518
+ }
45519
+ }
45520
+ },
45521
+ handler: async (input, ctx) => {
45522
+ const limit = input.limit ?? 50;
45523
+ const filters = [];
45524
+ if (input.employee) {
45525
+ filters.push([
45526
+ "employee",
45527
+ "=",
45528
+ await resolveEmployee(ctx.client, input.employee, {
45529
+ inputPath: "employee"
45530
+ })
45531
+ ]);
45532
+ }
45533
+ if (input.log_type) {
45534
+ filters.push(["log_type", "=", input.log_type]);
45535
+ }
45536
+ if (input.date_from) {
45537
+ filters.push(["time", ">=", `${input.date_from} 00:00:00`]);
45538
+ }
45539
+ if (input.date_to) {
45540
+ filters.push([
45541
+ "time",
45542
+ "<",
45543
+ `${nextDayISO(
45544
+ input.date_to,
45545
+ "erpnext_employee_checkin_list"
45546
+ )} 00:00:00`
45547
+ ]);
45548
+ }
45549
+ const docs = await ctx.client.list("Employee Checkin", {
45550
+ fields: [
45551
+ "name",
45552
+ "employee",
45553
+ "employee_name",
45554
+ "time",
45555
+ "log_type",
45556
+ "shift",
45557
+ "attendance",
45558
+ "skip_auto_attendance"
45559
+ ],
45560
+ filters,
45561
+ limit,
45562
+ order_by: "time asc"
45563
+ });
45564
+ return await listResult(ctx, "Employee Checkin", docs, {
45565
+ filters,
45566
+ limit
45567
+ });
45568
+ }
45569
+ },
45570
+ {
45571
+ name: "erpnext_attendance_day_get",
45572
+ annotations: { readOnlyHint: true },
45573
+ description: "Read one employee's full attendance picture for ONE day: every punch, the shift that governs it, the minutes worked as this site computes them, and the Attendance record standing in the way of a repair. Read this before calling erpnext_attendance_day_fix: 'locked' true means the day already carries a submitted Attendance, so fixing it CANCELS that record and rebuilds it, and the fix tool will refuse without confirm_cancel_attendance. 'blocked' true means the fix cannot proceed at all (a draft Attendance, or one backed by a still-valid Attendance Request); 'locked_reason' says which. 'is_self' true means the API user may not edit this employee - the site enforces segregation of duties. " + REQUIRES_HVG_WORKSPACE,
45574
+ category: "hr",
45575
+ inputSchema: {
45576
+ type: "object",
45577
+ properties: {
45578
+ employee: {
45579
+ type: "string",
45580
+ description: "Employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
45581
+ },
45582
+ date: { type: "string", description: "The day to read, YYYY-MM-DD" }
45583
+ },
45584
+ required: ["employee", "date"]
45585
+ },
45586
+ handler: async (input, ctx) => {
45587
+ if (!input.employee) {
45588
+ throw new Error("[erpnext_attendance_day_get] 'employee' is required");
45589
+ }
45590
+ if (!input.date) {
45591
+ throw new Error("[erpnext_attendance_day_get] 'date' is required");
45592
+ }
45593
+ const employee = await resolveEmployee(
45594
+ ctx.client,
45595
+ input.employee,
45596
+ { inputPath: "employee" }
45597
+ );
45598
+ const state = await readAttendanceDay(
45599
+ ctx,
45600
+ employee,
45601
+ input.date,
45602
+ "erpnext_attendance_day_get"
45603
+ );
45604
+ return { data: state };
45605
+ }
45606
+ },
45607
+ {
45608
+ name: "erpnext_attendance_day_fix",
45609
+ annotations: {
45610
+ readOnlyHint: false,
45611
+ // Huỷ một bản Attendance đã duyệt là thao tác không hoàn tác được bằng chính tool
45612
+ // này, nên hint phải nói thật kể cả khi phần lớn lượt gọi chỉ thêm một lượt bấm.
45613
+ destructiveHint: true,
45614
+ idempotentHint: false
45615
+ },
45616
+ description: "Repair ONE employee's attendance for ONE day: add the missing punches, correct the wrong ones, and rebuild that day's Attendance from the result. This is the tool for the 'a day is missing its check-out' case. Adding an Employee Checkin on its own does NOT fix such a day - HRMS refuses to build a second Attendance for a day that already has one and silently marks the new punch skipped - so the day is repaired by cancelling the stale Attendance and recomputing, which is what this tool does in a single transaction. Pass punches to create in 'add' and corrections in 'edit'; punches you do not mention are left alone. Deleting a punch is not possible here by design. 'reason' is written to the audit trail and is required. When the day already carries a submitted Attendance, the call is refused unless confirm_cancel_attendance is true, because saving cancels that record. Refuses outright for a future date, a timestamp that has not happened yet, an unusable log_type, a draft Attendance on the day, or an Attendance backed by a still-valid Attendance Request. It also refuses when a punch already on the day carries a blank log_type: name that punch in 'edit' with the direction it should carry, because this tool does not guess one. " + REQUIRES_HVG_WORKSPACE,
45617
+ category: "hr",
45618
+ inputSchema: {
45619
+ type: "object",
45620
+ properties: {
45621
+ employee: {
45622
+ type: "string",
45623
+ description: "Employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
45624
+ },
45625
+ date: { type: "string", description: "The day to repair, YYYY-MM-DD" },
45626
+ reason: {
45627
+ type: "string",
45628
+ description: `Why the punches are being changed, at least ${MIN_FIX_REASON} characters. Stored on the audit trail; it is the only record of why the hours moved.`
45629
+ },
45630
+ add: {
45631
+ type: "array",
45632
+ description: "Punches to create on this day.",
45633
+ items: {
45634
+ type: "object",
45635
+ properties: {
45636
+ log_type: {
45637
+ type: "string",
45638
+ description: "Direction of the punch",
45639
+ enum: ["IN", "OUT"]
45640
+ },
45641
+ time: {
45642
+ type: "string",
45643
+ description: "When the punch happened, 'YYYY-MM-DD HH:MM:SS'. Must fall inside the day's shift window - see 'shift.window_start' / 'shift.window_end' from erpnext_attendance_day_get."
45644
+ }
45645
+ },
45646
+ required: ["log_type", "time"]
45647
+ }
45648
+ },
45649
+ edit: {
45650
+ type: "array",
45651
+ description: "Corrections to punches that already exist. Identify each by its Employee Checkin name from erpnext_attendance_day_get.",
45652
+ items: {
45653
+ type: "object",
45654
+ properties: {
45655
+ name: {
45656
+ type: "string",
45657
+ description: "Employee Checkin ID, e.g. 'EMP-CKIN-08-2026-00123'"
45658
+ },
45659
+ log_type: {
45660
+ type: "string",
45661
+ description: "New direction; omit to keep the current one",
45662
+ enum: ["IN", "OUT"]
45663
+ },
45664
+ time: {
45665
+ type: "string",
45666
+ description: "New timestamp 'YYYY-MM-DD HH:MM:SS'; omit to keep the current one"
45667
+ }
45668
+ },
45669
+ required: ["name"]
45670
+ }
45671
+ },
45672
+ confirm_cancel_attendance: {
45673
+ type: "boolean",
45674
+ description: "Acknowledge that the day's submitted Attendance will be cancelled and rebuilt. Required only when erpnext_attendance_day_get reports 'locked': true.",
45675
+ default: false
45676
+ }
45677
+ },
45678
+ required: ["employee", "date", "reason"]
45679
+ },
45680
+ handler: async (input, ctx) => {
45681
+ const TOOL = "erpnext_attendance_day_fix";
45682
+ if (!input.employee) throw new Error(`[${TOOL}] 'employee' is required`);
45683
+ if (!input.date) throw new Error(`[${TOOL}] 'date' is required`);
45684
+ const reason = String(input.reason ?? "").trim();
45685
+ if (reason.length < MIN_FIX_REASON) {
45686
+ throw new Error(
45687
+ `[${TOOL}] 'reason' must be at least ${MIN_FIX_REASON} characters. It is the only thing that explains the change to whoever reads the audit trail later.`
45688
+ );
45689
+ }
45690
+ const adds = input.add ?? [];
45691
+ const edits = input.edit ?? [];
45692
+ if (adds.length === 0 && edits.length === 0) {
45693
+ throw new Error(
45694
+ `[${TOOL}] nothing to do: pass at least one punch in 'add' or 'edit'.`
45695
+ );
45696
+ }
45697
+ for (const row of adds) {
45698
+ assertLogType(row.log_type, TOOL);
45699
+ if (!row.time) {
45700
+ throw new Error(`[${TOOL}] every entry in 'add' needs a 'time'.`);
45701
+ }
45702
+ }
45703
+ for (const row of edits) {
45704
+ if (!row.name) {
45705
+ throw new Error(
45706
+ `[${TOOL}] every entry in 'edit' needs the Employee Checkin 'name'.`
45707
+ );
45708
+ }
45709
+ if (row.log_type !== void 0) assertLogType(row.log_type, TOOL);
45710
+ }
45711
+ const date3 = input.date;
45712
+ const { now, authoritative: exactClock } = await siteNow(ctx);
45713
+ const today = now.slice(0, 10);
45714
+ if (date3 > today) {
45715
+ throw new Error(
45716
+ `[${TOOL}] '${date3}' is in the future (site today is ${today}). Attendance can only be repaired for a day that has already happened.`
45717
+ );
45718
+ }
45719
+ const employee = await resolveLink(
45720
+ ctx.client,
45721
+ "Employee",
45722
+ input.employee,
45723
+ "employee_name",
45724
+ // Write path — see purchasing.ts: no fuzzy matching on writes.
45725
+ { allowPartialMatch: false, inputPath: "employee" }
45726
+ );
45727
+ const state = await readAttendanceDay(ctx, employee, date3, TOOL);
45728
+ const current = state.rows ?? [];
45729
+ if (state.blocked) {
45730
+ throw new Error(
45731
+ `[${TOOL}] ${date3} cannot be repaired as it stands. ` + (state.locked_reason ?? "The site reports the day as blocked.") + " Resolve that in ERPNext first."
45732
+ );
45733
+ }
45734
+ if (state.locked && input.confirm_cancel_attendance !== true) {
45735
+ throw new Error(
45736
+ `[${TOOL}] ${date3} already has an Attendance record` + (state.attendance?.name ? ` (${state.attendance.name})` : "") + ". Saving cancels it and rebuilds the day from the punches. Pass confirm_cancel_attendance: true to proceed. " + (state.locked_reason ? `Site says: ${state.locked_reason}` : "")
45737
+ );
45738
+ }
45739
+ const byName = new Map(current.map((row) => [row.name, row]));
45740
+ for (const row of edits) {
45741
+ if (!byName.has(row.name)) {
45742
+ throw new Error(
45743
+ `[${TOOL}] checkin '${row.name}' is not one of ${date3}'s punches. Read them with erpnext_attendance_day_get first.`
45744
+ );
45745
+ }
45746
+ }
45747
+ const edited = /* @__PURE__ */ new Map();
45748
+ for (const row of edits) {
45749
+ const name = row.name;
45750
+ if (edited.has(name)) {
45751
+ throw new Error(
45752
+ `[${TOOL}] checkin '${name}' appears twice in 'edit'. Put every change to one punch in a single entry.`
45753
+ );
45754
+ }
45755
+ edited.set(name, row);
45756
+ }
45757
+ const target = current.map((row) => {
45758
+ const patch = edited.get(row.name);
45759
+ return {
45760
+ name: row.name,
45761
+ time: normalizeStamp(patch?.time ?? row.time),
45762
+ log_type: patch?.log_type ?? row.log_type
45763
+ };
45764
+ });
45765
+ for (const row of adds) {
45766
+ target.push({
45767
+ name: "",
45768
+ time: normalizeStamp(row.time),
45769
+ log_type: row.log_type
45770
+ });
45771
+ }
45772
+ const rows = orderedPunches(target, TOOL);
45773
+ const future = exactClock ? rows.filter((row) => row.time > now) : [];
45774
+ if (future.length > 0) {
45775
+ throw new Error(
45776
+ `[${TOOL}] these punches are still in the future (site now is ${now}): ` + future.map((row) => row.time).join(", ") + ". Attendance can only record time that has already been worked."
45777
+ );
45778
+ }
45779
+ const base = {};
45780
+ for (const row of current) {
45781
+ base[row.name] = { time: row.time, log_type: row.log_type ?? "" };
45782
+ }
45783
+ const saved = await callHvgWorkspace(
45784
+ ctx,
45785
+ "hvg_workspace.api.hr_save_day_attendance",
45786
+ { employee, date: date3, reason, rows, base },
45787
+ TOOL
45788
+ );
45789
+ ctx.client.invalidate("Employee Checkin");
45790
+ for (const row of current) {
45791
+ ctx.client.invalidate("Employee Checkin", row.name);
45792
+ }
45793
+ for (const name of saved.cancelled_attendance ?? []) {
45794
+ ctx.client.invalidate("Attendance", name);
45795
+ }
45796
+ ctx.client.invalidate("Attendance", saved.attendance?.name);
45797
+ const cancelled = saved.cancelled_attendance ?? [];
45798
+ const parts = [
45799
+ `Attendance for ${employee} on ${date3} rebuilt`,
45800
+ `${saved.changed_count ?? 0} punch(es) written`
45801
+ ];
45802
+ if (cancelled.length > 0) {
45803
+ parts.push(`cancelled ${cancelled.join(", ")}`);
45804
+ }
45805
+ if (cancelled.length > 0 && input.confirm_cancel_attendance !== true) {
45806
+ parts.push(
45807
+ `WARNING: ${cancelled.join(", ")} was cancelled even though this call did not confirm a cancellation - the record appeared between the read and the save. Check that the rebuilt day is what you expected`
45808
+ );
45809
+ }
45810
+ if (saved.attendance?.name) {
45811
+ parts.push(
45812
+ `now ${saved.attendance.name} ${saved.attendance.status} (${saved.attendance.working_hours ?? 0}h)`
45813
+ );
45814
+ }
45815
+ if (saved.recompute?.skipped) {
45816
+ parts.push(
45817
+ `WARNING: the day could not be recomputed (${saved.recompute.skipped}), so it still has no Attendance record`
45818
+ );
45819
+ }
45820
+ if (saved.no_change) {
45821
+ parts.push("no punch actually differed from what was already stored");
45822
+ }
45823
+ return { data: saved, message: `${parts.join("; ")}.` };
45824
+ }
45825
+ },
45408
45826
  // ── Leave Applications ────────────────────────────────────────────────────
45409
45827
  {
45410
45828
  name: "erpnext_leave_application_list",
@@ -46037,7 +46455,9 @@ var TASK_LIST_FIELDS = [
46037
46455
  "progress"
46038
46456
  ];
46039
46457
  var TASK_SKU_FIELD = "custom_sku";
46040
- var taskSkuSupport = /* @__PURE__ */ new WeakMap();
46458
+ var TASK_PRODUCT_TYPE_FIELD = "custom_product_type";
46459
+ var TASK_OPTIONAL_FIELDS = [TASK_SKU_FIELD, TASK_PRODUCT_TYPE_FIELD];
46460
+ var taskMissingFields = /* @__PURE__ */ new WeakMap();
46041
46461
  var UNKNOWN_COLUMN_RE = /Unknown column \\?['"`]([^'"`\\]+)/i;
46042
46462
  function isUnknownColumnError(error2, field) {
46043
46463
  if (!(error2 instanceof FrappeAPIError)) return false;
@@ -46135,7 +46555,7 @@ var projectTools = [
46135
46555
  name: "erpnext_task_list",
46136
46556
  annotations: { readOnlyHint: true },
46137
46557
  _meta: DOCLIST_META,
46138
- description: "List Tasks. Filterable by project, status, priority. Fields: name, subject, project, status, priority, exp_start_date, exp_end_date, progress. Sites carrying hvg_workspace also get 'custom_sku'; it is absent elsewhere. Where present it is empty for every Task created before that field shipped and never backfilled, so an empty value means 'not recorded here', never 'no product'.",
46558
+ description: "List Tasks. Filterable by project, status, priority. Fields: name, subject, project, status, priority, exp_start_date, exp_end_date, progress. Sites carrying hvg_workspace also get 'custom_sku' and 'custom_product_type'; both are absent elsewhere, and a site may carry the first without the second. Where present they are empty for every Task created before each field shipped and never backfilled, so an empty value means 'not recorded here', never 'no product'.",
46139
46559
  category: "project",
46140
46560
  inputSchema: {
46141
46561
  type: "object",
@@ -46194,23 +46614,31 @@ var projectTools = [
46194
46614
  filters.push(["exp_end_date", "<=", input.date_to]);
46195
46615
  }
46196
46616
  const query = { filters, limit, order_by: "modified desc" };
46197
- const askForSku = taskSkuSupport.get(ctx.client) !== false;
46617
+ const missing = taskMissingFields.get(ctx.client);
46618
+ let optional2 = TASK_OPTIONAL_FIELDS.filter(
46619
+ (field) => !missing?.has(field)
46620
+ );
46198
46621
  let docs;
46199
- try {
46200
- docs = await ctx.client.list("Task", {
46201
- fields: askForSku ? [...TASK_LIST_FIELDS, TASK_SKU_FIELD] : TASK_LIST_FIELDS,
46202
- ...query
46203
- });
46204
- if (askForSku) taskSkuSupport.set(ctx.client, true);
46205
- } catch (error2) {
46206
- if (!askForSku || !isUnknownColumnError(error2, TASK_SKU_FIELD)) {
46207
- throw error2;
46622
+ for (; ; ) {
46623
+ try {
46624
+ docs = await ctx.client.list("Task", {
46625
+ fields: [...TASK_LIST_FIELDS, ...optional2],
46626
+ ...query
46627
+ });
46628
+ break;
46629
+ } catch (error2) {
46630
+ const absent = optional2.find(
46631
+ (field) => isUnknownColumnError(error2, field)
46632
+ );
46633
+ if (absent === void 0) throw error2;
46634
+ let known = taskMissingFields.get(ctx.client);
46635
+ if (known === void 0) {
46636
+ known = /* @__PURE__ */ new Set();
46637
+ taskMissingFields.set(ctx.client, known);
46638
+ }
46639
+ known.add(absent);
46640
+ optional2 = optional2.filter((field) => field !== absent);
46208
46641
  }
46209
- taskSkuSupport.set(ctx.client, false);
46210
- docs = await ctx.client.list("Task", {
46211
- fields: TASK_LIST_FIELDS,
46212
- ...query
46213
- });
46214
46642
  }
46215
46643
  return await listResult(ctx, "Task", docs, {
46216
46644
  filters,
@@ -49458,11 +49886,205 @@ var setupTools = [
49458
49886
  }
49459
49887
  ];
49460
49888
 
49461
- // src/tools/analytics.ts
49462
- var MAX_RADAR_ITEMS = 8;
49463
- var MAX_PL_MONTHS = 60;
49889
+ // src/tools/analytics-dates.ts
49890
+ var MONTH_NAMES = [
49891
+ "Jan",
49892
+ "Feb",
49893
+ "Mar",
49894
+ "Apr",
49895
+ "May",
49896
+ "Jun",
49897
+ "Jul",
49898
+ "Aug",
49899
+ "Sep",
49900
+ "Oct",
49901
+ "Nov",
49902
+ "Dec"
49903
+ ];
49904
+ function analyticsDateWindow(today, monthsBack = 1) {
49905
+ const [year, month, day] = today.split("-").map(Number);
49906
+ const date3 = (monthIndex, dayOfMonth = 1) => new Date(Date.UTC(year, monthIndex, dayOfMonth)).toISOString().slice(0, 10);
49907
+ const monthKeys = [];
49908
+ const labels = [];
49909
+ for (let offset = 0; offset < monthsBack; offset++) {
49910
+ const start = date3(month - monthsBack + offset);
49911
+ monthKeys.push(start.slice(0, 7));
49912
+ labels.push(
49913
+ `${MONTH_NAMES[Number(start.slice(5, 7)) - 1]} ${start.slice(2, 4)}`
49914
+ );
49915
+ }
49916
+ return {
49917
+ start: date3(month - monthsBack),
49918
+ end: today,
49919
+ nextDay: date3(month - 1, day + 1),
49920
+ previousStart: date3(month - 2),
49921
+ previousEnd: date3(month - 1, 0),
49922
+ quarterStart: date3(Math.floor((month - 1) / 3) * 3),
49923
+ yearStart: date3(0),
49924
+ monthKeys,
49925
+ labels
49926
+ };
49927
+ }
49928
+ function analyticsMonthIndex(date3, monthKeys) {
49929
+ return monthKeys.indexOf(date3.slice(0, 7));
49930
+ }
49931
+
49932
+ // src/tools/analytics-context.ts
49933
+ var SCOPE_REQUEST_TARGET_BUDGET = 6e3;
49934
+ function listRequestTargetLength(doctype, options) {
49935
+ const params = new URLSearchParams();
49936
+ if (options.fields?.length) {
49937
+ params.set("fields", JSON.stringify(options.fields));
49938
+ }
49939
+ if (options.filters?.length) {
49940
+ params.set("filters", JSON.stringify(options.filters));
49941
+ }
49942
+ if (options.order_by) params.set("order_by", options.order_by);
49943
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
49944
+ if (options.limit_start !== void 0) {
49945
+ params.set("limit_start", String(options.limit_start));
49946
+ }
49947
+ params.set("as_dict", "1");
49948
+ return `/api/resource/${encodeURIComponent(doctype)}?${params}`.length;
49949
+ }
49950
+ function chunkRequestNames(doctype, names, queryOptions) {
49951
+ const chunks = [];
49952
+ let current = [];
49953
+ for (const name of new Set(names)) {
49954
+ const candidate = [...current, name];
49955
+ if (listRequestTargetLength(doctype, queryOptions(candidate)) <= SCOPE_REQUEST_TARGET_BUDGET) {
49956
+ current = candidate;
49957
+ continue;
49958
+ }
49959
+ if (current.length) chunks.push(current);
49960
+ current = [name];
49961
+ if (listRequestTargetLength(doctype, queryOptions(current)) > SCOPE_REQUEST_TARGET_BUDGET) {
49962
+ throw new Error(
49963
+ `Analytics ${doctype} scope cannot fit within the encoded request budget.`
49964
+ );
49965
+ }
49966
+ }
49967
+ if (current.length) chunks.push(current);
49968
+ return chunks;
49969
+ }
49970
+ async function listAnalyticsItemUnits(ctx, names) {
49971
+ const queryOptions = (chunk) => ({
49972
+ fields: ["name", "stock_uom"],
49973
+ filters: [["name", "in", chunk]],
49974
+ limit: chunk.length,
49975
+ order_by: "name asc"
49976
+ });
49977
+ const chunks = chunkRequestNames("Item", names, queryOptions);
49978
+ if (names.length > ANALYTICS_MAX_ROWS || chunks.length > ANALYTICS_MAX_REQUESTS) {
49979
+ throw new Error(
49980
+ "Analytics Item UOM lookup exceeds its safety budget; no partial chart will be reported."
49981
+ );
49982
+ }
49983
+ const result = [];
49984
+ for (const chunk of chunks) {
49985
+ const rows = await ctx.client.list("Item", queryOptions(chunk));
49986
+ const remaining = new Set(chunk);
49987
+ for (const row of rows) {
49988
+ if (typeof row?.name !== "string" || !remaining.delete(row.name)) {
49989
+ throw new Error(
49990
+ "Analytics Item lookup returned a duplicate or unrequested name."
49991
+ );
49992
+ }
49993
+ }
49994
+ if (remaining.size) {
49995
+ throw new Error(
49996
+ "Analytics Item lookup is missing a requested item; verified stock UOM is unavailable."
49997
+ );
49998
+ }
49999
+ result.push(...rows);
50000
+ }
50001
+ return result;
50002
+ }
50003
+ function sortScopedRows(rows, sortField) {
50004
+ const candidates = rows.map((row) => {
50005
+ let value;
50006
+ if (sortField === "modified") {
50007
+ const modified = row.modified;
50008
+ if (typeof modified !== "string" || !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$/.test(modified)) {
50009
+ throw new Error(
50010
+ "Analytics scoped row has no valid modified timestamp for ordered merging."
50011
+ );
50012
+ }
50013
+ value = modified.includes(".") ? modified.padEnd(26, "0") : `${modified}.000000`;
50014
+ } else value = analyticsNumber(row, sortField);
50015
+ return { row, value };
50016
+ });
50017
+ candidates.sort((a, b) => a.value < b.value ? 1 : a.value > b.value ? -1 : 0);
50018
+ return candidates.map(({ row }) => row);
50019
+ }
50020
+ async function listScoped(ctx, doctype, options, scopeField, names, verify, extraFilters = [], completeBudget) {
50021
+ const limit = normalizeLimit(options.limit ?? 20);
50022
+ if (options.limit_start !== void 0 && options.limit_start !== 0) {
50023
+ throw new Error("Analytics scoped reads do not support offsets.");
50024
+ }
50025
+ const orderBy = options.order_by ?? "modified desc";
50026
+ if (!["base_amount desc", "stock_value desc", "modified desc"].includes(orderBy)) {
50027
+ throw new Error(`Unsupported analytics scope order: '${orderBy}'.`);
50028
+ }
50029
+ const sortField = orderBy.split(" ")[0];
50030
+ const queryOptions = (scopeNames) => ({
50031
+ ...options,
50032
+ fields: [.../* @__PURE__ */ new Set([...options.fields ?? [], sortField])],
50033
+ filters: [
50034
+ ...options.filters ?? [],
50035
+ [scopeField, "in", scopeNames],
50036
+ ...extraFilters
50037
+ ],
50038
+ limit,
50039
+ order_by: orderBy
50040
+ });
50041
+ const chunks = chunkRequestNames(
50042
+ doctype,
50043
+ names,
50044
+ (chunk) => completeBudget ? completePageOptions(queryOptions(chunk), ANALYTICS_MAX_ROWS) : queryOptions(chunk)
50045
+ );
50046
+ if (completeBudget) {
50047
+ const result2 = [];
50048
+ const seen = /* @__PURE__ */ new Set();
50049
+ for (const chunk of chunks) {
50050
+ const allowed = new Set(chunk);
50051
+ const rows = await listCompleteAnalytics(
50052
+ async (type, page) => {
50053
+ const rows2 = await ctx.client.list(type, page);
50054
+ for (const row of rows2) verify(row, allowed);
50055
+ return rows2;
50056
+ },
50057
+ doctype,
50058
+ queryOptions(chunk),
50059
+ completeBudget
50060
+ );
50061
+ for (const row of rows) {
50062
+ if (seen.has(row.name)) {
50063
+ throw new Error(
50064
+ `Analytics ${doctype} returned a duplicate row across scope chunks.`
50065
+ );
50066
+ }
50067
+ seen.add(row.name);
50068
+ result2.push(row);
50069
+ }
50070
+ }
50071
+ return chunks.length > 1 ? sortScopedRows(result2, sortField) : result2;
50072
+ }
50073
+ let result = [];
50074
+ for (const chunk of chunks) {
50075
+ const rows = await ctx.client.list(doctype, queryOptions(chunk));
50076
+ const allowed = new Set(chunk);
50077
+ for (const row of rows) verify(row, allowed);
50078
+ if (chunks.length === 1) return rows.slice(0, limit);
50079
+ result = sortScopedRows([...result, ...rows], sortField).slice(0, limit);
50080
+ }
50081
+ return result;
50082
+ }
49464
50083
  async function resolveReportCompany(ctx, input) {
49465
- if (typeof input.company === "string" && input.company.trim() !== "") {
50084
+ if (input.company !== void 0) {
50085
+ if (typeof input.company !== "string" || input.company.trim() === "") {
50086
+ throw new Error("'company' must be a non-empty Company name.");
50087
+ }
49466
50088
  return input.company.trim();
49467
50089
  }
49468
50090
  const companies = await ctx.client.list("Company", {
@@ -49470,17 +50092,201 @@ async function resolveReportCompany(ctx, input) {
49470
50092
  limit: 21,
49471
50093
  order_by: "name asc"
49472
50094
  });
49473
- const names = companies.map((row) => row.name).filter((name) => typeof name === "string");
50095
+ const names = companies.map((row) => row.name);
50096
+ if (names.some((name) => typeof name !== "string" || name.trim() === "")) {
50097
+ throw new Error("Company lookup returned an invalid name.");
50098
+ }
49474
50099
  if (names.length === 1) return names[0];
49475
50100
  if (names.length === 0) {
49476
50101
  throw new Error(
49477
- "[erpnext_profit_loss] no Company is visible to you, so there is no chart of accounts to report on. Ask an administrator for access to a company."
50102
+ "No Company is visible to you. Ask an administrator for access to a company."
49478
50103
  );
49479
50104
  }
49480
50105
  throw new Error(
49481
- `[erpnext_profit_loss] this site has ${names.length > 20 ? "more than 20" : names.length} companies, so 'company' is required. Pass one of: ${names.slice(0, 20).join(", ")}${names.length > 20 ? ", ..." : ""}.`
50106
+ `Multiple companies are visible, so 'company' is required. Pass one of: ${names.slice(0, 20).join(", ")}${names.length > 20 ? ", ..." : ""}.`
49482
50107
  );
49483
50108
  }
50109
+ function analyticsNumber(row, field) {
50110
+ const value = cellNumber(row[field]);
50111
+ if (value === null) {
50112
+ throw new Error(
50113
+ `Analytics field '${field}' is missing or not a finite number; refusing to report zero.`
50114
+ );
50115
+ }
50116
+ return value;
50117
+ }
50118
+ async function listOwnershipNames(listDocuments, doctype, filters = []) {
50119
+ const pageSize = 1e3;
50120
+ const maxNames = 1e5;
50121
+ const names = /* @__PURE__ */ new Set();
50122
+ for (let offset = 0; ; offset += pageSize) {
50123
+ const rows = await listDocuments(doctype, {
50124
+ fields: ["name"],
50125
+ filters,
50126
+ limit: pageSize,
50127
+ limit_start: offset,
50128
+ order_by: "name asc"
50129
+ });
50130
+ if (rows.length > pageSize) {
50131
+ throw new Error(
50132
+ `Analytics ${doctype} ownership page exceeds its requested size.`
50133
+ );
50134
+ }
50135
+ for (const row of rows) {
50136
+ const name = row?.name;
50137
+ if (typeof name !== "string" || name.trim() === "") {
50138
+ throw new Error(
50139
+ `Analytics ${doctype} ownership lookup returned an invalid name.`
50140
+ );
50141
+ }
50142
+ if (names.has(name)) {
50143
+ throw new Error(
50144
+ `Analytics ${doctype} ownership pagination did not make unique progress.`
50145
+ );
50146
+ }
50147
+ names.add(name);
50148
+ if (names.size > maxNames) {
50149
+ throw new Error(
50150
+ `Analytics ${doctype} ownership lookup exceeds the ${maxNames} name safety limit.`
50151
+ );
50152
+ }
50153
+ }
50154
+ if (rows.length < pageSize) return [...names];
50155
+ }
50156
+ }
50157
+ async function resolveAnalyticsContext(ctx, input) {
50158
+ const company = await resolveReportCompany(ctx, input);
50159
+ const doc = await ctx.client.get("Company", company);
50160
+ const currency = doc.default_currency;
50161
+ if (typeof currency !== "string" || !/^[A-Z]{3}$/.test(currency)) {
50162
+ throw new Error(`Company '${company}' has no valid default_currency.`);
50163
+ }
50164
+ const listDocuments = (doctype, options) => ctx.client.list(doctype, {
50165
+ ...options,
50166
+ filters: [...options.filters ?? [], ["company", "=", company]]
50167
+ });
50168
+ let warehouses;
50169
+ const budget = { requests: 0, rows: 0 };
50170
+ const listItems = async (parentType, options, complete = false) => {
50171
+ const parentFilters = (options.filters ?? []).filter(
50172
+ (filter) => filter[0] === "docstatus"
50173
+ );
50174
+ const names = await listOwnershipNames(
50175
+ listDocuments,
50176
+ parentType,
50177
+ parentFilters
50178
+ );
50179
+ if (names.length === 0) return [];
50180
+ return await listScoped(
50181
+ ctx,
50182
+ `${parentType} Item`,
50183
+ {
50184
+ ...options,
50185
+ fields: [
50186
+ .../* @__PURE__ */ new Set([...options.fields ?? [], "parent", "parenttype"])
50187
+ ]
50188
+ },
50189
+ "parent",
50190
+ names,
50191
+ (row, allowed) => {
50192
+ if (typeof row.parent !== "string" || !allowed.has(row.parent) || row.parenttype !== parentType) {
50193
+ throw new Error(
50194
+ `Analytics ${parentType} Item has unverified company ownership.`
50195
+ );
50196
+ }
50197
+ },
50198
+ [["parenttype", "=", parentType]],
50199
+ complete ? budget : void 0
50200
+ );
50201
+ };
50202
+ const listBins = async (options, complete = false) => {
50203
+ warehouses ??= listOwnershipNames(listDocuments, "Warehouse");
50204
+ const names = await warehouses;
50205
+ if (names.length === 0) return [];
50206
+ return await listScoped(
50207
+ ctx,
50208
+ "Bin",
50209
+ {
50210
+ ...options,
50211
+ fields: [.../* @__PURE__ */ new Set([...options.fields ?? [], "warehouse"])]
50212
+ },
50213
+ "warehouse",
50214
+ names,
50215
+ (row, allowed) => {
50216
+ if (typeof row.warehouse !== "string" || !allowed.has(row.warehouse)) {
50217
+ throw new Error("Analytics Bin has unverified company ownership.");
50218
+ }
50219
+ },
50220
+ [],
50221
+ complete ? budget : void 0
50222
+ );
50223
+ };
50224
+ return {
50225
+ company,
50226
+ currency,
50227
+ listDocuments,
50228
+ listItems,
50229
+ listBins,
50230
+ listAllDocuments: (doctype, options) => listCompleteAnalytics(listDocuments, doctype, options, budget),
50231
+ listAllItems: (parentType, options) => listItems(parentType, options, true),
50232
+ listAllBins: (options) => listBins(options, true)
50233
+ };
50234
+ }
50235
+ function companyAnalyticsTool(tool) {
50236
+ return {
50237
+ ...tool,
50238
+ inputSchema: {
50239
+ ...tool.inputSchema,
50240
+ properties: {
50241
+ ...tool.inputSchema.properties,
50242
+ company: {
50243
+ type: "string",
50244
+ description: "Company name. Optional only when exactly one Company is visible. Monetary values use its default currency."
50245
+ }
50246
+ }
50247
+ },
50248
+ handler: async (input, ctx) => {
50249
+ const maxItems = tool.inputSchema.properties?.items?.maxItems;
50250
+ if (Array.isArray(input.items) && typeof maxItems === "number" && input.items.length > maxItems) {
50251
+ throw new Error(
50252
+ `${tool.name} accepts at most ${maxItems} item codes, received ${input.items.length}.`
50253
+ );
50254
+ }
50255
+ const context2 = await resolveAnalyticsContext(ctx, input);
50256
+ const result = await tool.handler(input, ctx, context2);
50257
+ if (result === null || typeof result !== "object" || Array.isArray(result)) {
50258
+ throw new Error(`${tool.name} returned an invalid analytics result.`);
50259
+ }
50260
+ return {
50261
+ ...result,
50262
+ refreshRequest: {
50263
+ toolName: tool.name,
50264
+ arguments: { ...input, company: context2.company }
50265
+ }
50266
+ };
50267
+ }
50268
+ };
50269
+ }
50270
+
50271
+ // src/tools/analytics.ts
50272
+ var MAX_RADAR_ITEMS = 8;
50273
+ var MAX_PL_MONTHS = 60;
50274
+ var MAX_REVENUE_MONTHS = 60;
50275
+ function revenueTrendTool(tool) {
50276
+ const wrapped = companyAnalyticsTool(tool);
50277
+ return {
50278
+ ...wrapped,
50279
+ handler: async (input, ctx) => {
50280
+ const months = input.months === void 0 ? 6 : input.months;
50281
+ if (typeof months !== "number" || !Number.isInteger(months) || months < 1 || months > MAX_REVENUE_MONTHS) {
50282
+ throw new Error(
50283
+ `[erpnext_revenue_trend] 'months' must be a whole number between 1 and ${MAX_REVENUE_MONTHS}`
50284
+ );
50285
+ }
50286
+ return await wrapped.handler(input, ctx);
50287
+ }
50288
+ };
50289
+ }
49484
50290
  function utcDateString(instant) {
49485
50291
  return instant.toISOString().slice(0, 10);
49486
50292
  }
@@ -49525,27 +50331,31 @@ var analyticsTools = [
49525
50331
  }
49526
50332
  let allowedItems = null;
49527
50333
  if (input.item_group) {
49528
- const groupItems = await ctx.client.list("Item", {
49529
- fields: ["name"],
49530
- filters: [["item_group", "=", input.item_group]],
49531
- limit: 1e3
49532
- });
50334
+ const groupItems = await listCompleteAnalytics(
50335
+ ctx.client.list.bind(ctx.client),
50336
+ "Item",
50337
+ {
50338
+ fields: ["name"],
50339
+ filters: [["item_group", "=", input.item_group]]
50340
+ }
50341
+ );
49533
50342
  allowedItems = new Set(groupItems.map((i) => i.name));
49534
50343
  }
49535
- const bins = await ctx.client.list("Bin", {
49536
- fields: ["item_code", "warehouse", "actual_qty", "stock_value"],
49537
- filters,
49538
- // widen the fetch when filtering by group in memory, then slice below
49539
- limit: allowedItems ? 1e3 : limit,
49540
- order_by: "actual_qty desc"
49541
- });
50344
+ const bins = await listCompleteAnalytics(
50345
+ ctx.client.list.bind(ctx.client),
50346
+ "Bin",
50347
+ {
50348
+ fields: ["item_code", "warehouse", "actual_qty"],
50349
+ filters,
50350
+ order_by: "actual_qty desc"
50351
+ }
50352
+ );
49542
50353
  const byItem = {};
49543
50354
  for (const bin of bins) {
49544
50355
  const item = bin.item_code;
49545
50356
  if (allowedItems && !allowedItems.has(item)) continue;
49546
- if (!byItem[item]) byItem[item] = { qty: 0, value: 0 };
49547
- byItem[item].qty += Number(bin.actual_qty) || 0;
49548
- byItem[item].value += Number(bin.stock_value) || 0;
50357
+ if (!byItem[item]) byItem[item] = { qty: 0 };
50358
+ byItem[item].qty += analyticsNumber(bin, "actual_qty");
49549
50359
  }
49550
50360
  const sorted = Object.entries(byItem).sort(([, a], [, b]) => b.qty - a.qty).slice(0, limit);
49551
50361
  const warehouseLabel = input.warehouse ?? "All Warehouses";
@@ -49569,7 +50379,7 @@ var analyticsTools = [
49569
50379
  }
49570
50380
  },
49571
50381
  // ── Sales Chart ───────────────────────────────────────────────────────────
49572
- {
50382
+ companyAnalyticsTool({
49573
50383
  name: "erpnext_sales_chart",
49574
50384
  annotations: { readOnlyHint: true },
49575
50385
  _meta: CHART_META,
@@ -49594,25 +50404,26 @@ var analyticsTools = [
49594
50404
  }
49595
50405
  }
49596
50406
  },
49597
- handler: async (input, ctx) => {
50407
+ handler: async (input, _ctx, context2) => {
50408
+ const { currency } = context2;
50409
+ if (input.include_drafts !== void 0 && typeof input.include_drafts !== "boolean") {
50410
+ throw new Error("'include_drafts' must be a boolean");
50411
+ }
49598
50412
  const limit = normalizeLimit(input.limit ?? 10);
49599
50413
  const groupBy = input.group_by ?? "customer";
49600
- const filters = [];
49601
- if (!input.include_drafts) {
49602
- filters.push(["docstatus", "=", 1]);
49603
- }
50414
+ const filters = [
50415
+ input.include_drafts === true ? ["docstatus", "in", [0, 1]] : ["docstatus", "=", 1]
50416
+ ];
49604
50417
  if (groupBy === "status") {
49605
- const invoices2 = await ctx.client.list("Sales Invoice", {
49606
- fields: ["name", "status", "grand_total"],
49607
- filters: [["docstatus", "!=", 2]],
49608
- // exclude cancelled
49609
- limit: 500,
50418
+ const invoices2 = await context2.listAllDocuments("Sales Invoice", {
50419
+ fields: ["name", "status", "base_grand_total"],
50420
+ filters,
49610
50421
  order_by: "modified desc"
49611
50422
  });
49612
50423
  const byStatus = {};
49613
50424
  for (const inv of invoices2) {
49614
50425
  const s = inv.status ?? "Unknown";
49615
- byStatus[s] = (byStatus[s] ?? 0) + (Number(inv.grand_total) || 0);
50426
+ byStatus[s] = (byStatus[s] ?? 0) + analyticsNumber(inv, "base_grand_total");
49616
50427
  }
49617
50428
  const sorted2 = Object.entries(byStatus).sort(([, a], [, b]) => b - a);
49618
50429
  return {
@@ -49620,17 +50431,16 @@ var analyticsTools = [
49620
50431
  type: "donut",
49621
50432
  labels: sorted2.map(([s]) => s),
49622
50433
  datasets: [{ label: "Revenue", values: sorted2.map(([, v]) => v) }],
49623
- currency: "EUR",
50434
+ currency,
49624
50435
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
49625
50436
  _meta: CHART_META
49626
50437
  };
49627
50438
  }
49628
50439
  if (groupBy === "item") {
49629
- const items = await ctx.client.list("Sales Invoice Item", {
49630
- fields: ["item_code", "item_name", "amount"],
49631
- filters: [["docstatus", "=", 1]],
49632
- limit: 500,
49633
- order_by: "amount desc"
50440
+ const items = await context2.listAllItems("Sales Invoice", {
50441
+ fields: ["item_code", "item_name", "base_amount"],
50442
+ filters,
50443
+ order_by: "base_amount desc"
49634
50444
  });
49635
50445
  const byItem = {};
49636
50446
  for (const row of items) {
@@ -49641,7 +50451,7 @@ var analyticsTools = [
49641
50451
  total: 0
49642
50452
  };
49643
50453
  }
49644
- byItem[code].total += Number(row.amount) || 0;
50454
+ byItem[code].total += analyticsNumber(row, "base_amount");
49645
50455
  }
49646
50456
  const sorted2 = Object.entries(byItem).sort(([, a], [, b]) => b.total - a.total).slice(0, limit);
49647
50457
  return {
@@ -49654,15 +50464,14 @@ var analyticsTools = [
49654
50464
  values: sorted2.map(([, { total }]) => total),
49655
50465
  color: "#c084fc"
49656
50466
  }],
49657
- currency: "EUR",
50467
+ currency,
49658
50468
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
49659
50469
  _meta: CHART_META
49660
50470
  };
49661
50471
  }
49662
- const invoices = await ctx.client.list("Sales Invoice", {
49663
- fields: ["customer", "customer_name", "grand_total"],
50472
+ const invoices = await context2.listAllDocuments("Sales Invoice", {
50473
+ fields: ["customer", "customer_name", "base_grand_total"],
49664
50474
  filters,
49665
- limit: 500,
49666
50475
  order_by: "modified desc"
49667
50476
  });
49668
50477
  const byCustomer = {};
@@ -49674,7 +50483,7 @@ var analyticsTools = [
49674
50483
  total: 0
49675
50484
  };
49676
50485
  }
49677
- byCustomer[code].total += Number(inv.grand_total) || 0;
50486
+ byCustomer[code].total += analyticsNumber(inv, "base_grand_total");
49678
50487
  }
49679
50488
  const sorted = Object.entries(byCustomer).sort(([, a], [, b]) => b.total - a.total).slice(0, limit);
49680
50489
  return {
@@ -49687,14 +50496,14 @@ var analyticsTools = [
49687
50496
  values: sorted.map(([, { total }]) => total),
49688
50497
  color: "#4ade80"
49689
50498
  }],
49690
- currency: "EUR",
50499
+ currency,
49691
50500
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
49692
50501
  _meta: CHART_META
49693
50502
  };
49694
50503
  }
49695
- },
50504
+ }),
49696
50505
  // ── Revenue Trend (line / area) ─────────────────────────────────────────
49697
- {
50506
+ revenueTrendTool({
49698
50507
  name: "erpnext_revenue_trend",
49699
50508
  annotations: { readOnlyHint: true },
49700
50509
  _meta: CHART_META,
@@ -49704,8 +50513,11 @@ var analyticsTools = [
49704
50513
  type: "object",
49705
50514
  properties: {
49706
50515
  months: {
49707
- type: "number",
49708
- description: "How many months back to include (default 6)"
50516
+ type: "integer",
50517
+ minimum: 1,
50518
+ maximum: MAX_REVENUE_MONTHS,
50519
+ default: 6,
50520
+ description: `How many months back to include (default 6, max ${MAX_REVENUE_MONTHS})`
49709
50521
  },
49710
50522
  type: {
49711
50523
  type: "string",
@@ -49719,45 +50531,38 @@ var analyticsTools = [
49719
50531
  }
49720
50532
  }
49721
50533
  },
49722
- handler: async (input, ctx) => {
50534
+ handler: async (input, ctx, context2) => {
50535
+ const { currency } = context2;
49723
50536
  const monthsBack = input.months ?? 6;
49724
50537
  const chartType = input.type ?? "line";
49725
50538
  const groupBy = input.group_by ?? "total";
49726
- const now = /* @__PURE__ */ new Date();
49727
- const startDate = new Date(
49728
- now.getFullYear(),
49729
- now.getMonth() - monthsBack + 1,
49730
- 1
49731
- );
49732
- const startStr = startDate.toISOString().split("T")[0];
49733
- const orders = await ctx.client.list("Sales Order", {
49734
- fields: ["customer_name", "grand_total", "transaction_date"],
49735
- filters: [["transaction_date", ">=", startStr], ["docstatus", "!=", 2]],
49736
- limit: 1e3,
50539
+ const window2 = analyticsDateWindow(await siteToday(ctx), monthsBack);
50540
+ const orders = await context2.listAllDocuments("Sales Order", {
50541
+ fields: ["customer_name", "base_grand_total", "transaction_date"],
50542
+ filters: [
50543
+ ["transaction_date", ">=", window2.start],
50544
+ ["transaction_date", "<=", window2.end],
50545
+ ["docstatus", "!=", 2]
50546
+ ],
49737
50547
  order_by: "transaction_date asc"
49738
50548
  });
49739
- const months = [];
49740
- for (let m = 0; m < monthsBack; m++) {
49741
- const d = new Date(
49742
- now.getFullYear(),
49743
- now.getMonth() - monthsBack + 1 + m,
49744
- 1
49745
- );
49746
- months.push(
49747
- `${d.toLocaleString("en", { month: "short" })} ${d.getFullYear().toString().slice(2)}`
49748
- );
49749
- }
50549
+ const months = window2.labels;
49750
50550
  if (groupBy === "customer") {
49751
50551
  const byCustomerMonth = {};
49752
50552
  for (const order2 of orders) {
49753
- const d = new Date(order2.transaction_date);
49754
- const mIdx = (d.getFullYear() - startDate.getFullYear()) * 12 + d.getMonth() - startDate.getMonth();
50553
+ const mIdx = analyticsMonthIndex(
50554
+ order2.transaction_date,
50555
+ window2.monthKeys
50556
+ );
49755
50557
  if (mIdx < 0 || mIdx >= monthsBack) continue;
49756
50558
  const cust = order2.customer_name ?? "Unknown";
49757
50559
  if (!byCustomerMonth[cust]) {
49758
50560
  byCustomerMonth[cust] = new Array(monthsBack).fill(0);
49759
50561
  }
49760
- byCustomerMonth[cust][mIdx] += Number(order2.grand_total) || 0;
50562
+ byCustomerMonth[cust][mIdx] += analyticsNumber(
50563
+ order2,
50564
+ "base_grand_total"
50565
+ );
49761
50566
  }
49762
50567
  const sorted = Object.entries(byCustomerMonth).sort(
49763
50568
  ([, a], [, b]) => b.reduce((s, v) => s + v, 0) - a.reduce((s, v) => s + v, 0)
@@ -49775,17 +50580,19 @@ var analyticsTools = [
49775
50580
  showDots: chartType === "line",
49776
50581
  ...chartType === "stacked-area" ? { stack: "revenue" } : {}
49777
50582
  })),
49778
- currency: "EUR",
50583
+ currency,
49779
50584
  yAxisLabel: "Revenue",
49780
50585
  _meta: CHART_META
49781
50586
  };
49782
50587
  }
49783
50588
  const monthlyTotals = new Array(monthsBack).fill(0);
49784
50589
  for (const order2 of orders) {
49785
- const d = new Date(order2.transaction_date);
49786
- const mIdx = (d.getFullYear() - startDate.getFullYear()) * 12 + d.getMonth() - startDate.getMonth();
50590
+ const mIdx = analyticsMonthIndex(
50591
+ order2.transaction_date,
50592
+ window2.monthKeys
50593
+ );
49787
50594
  if (mIdx >= 0 && mIdx < monthsBack) {
49788
- monthlyTotals[mIdx] += Number(order2.grand_total) || 0;
50595
+ monthlyTotals[mIdx] += analyticsNumber(order2, "base_grand_total");
49789
50596
  }
49790
50597
  }
49791
50598
  return {
@@ -49799,14 +50606,14 @@ var analyticsTools = [
49799
50606
  color: "#60a5fa",
49800
50607
  showDots: true
49801
50608
  }],
49802
- currency: "EUR",
50609
+ currency,
49803
50610
  yAxisLabel: "Revenue",
49804
50611
  _meta: CHART_META
49805
50612
  };
49806
50613
  }
49807
- },
50614
+ }),
49808
50615
  // ── Order Breakdown (stacked bar / pie) ─────────────────────────────────
49809
- {
50616
+ companyAnalyticsTool({
49810
50617
  name: "erpnext_order_breakdown",
49811
50618
  annotations: { readOnlyHint: true },
49812
50619
  _meta: CHART_META,
@@ -49827,20 +50634,20 @@ var analyticsTools = [
49827
50634
  }
49828
50635
  }
49829
50636
  },
49830
- handler: async (input, ctx) => {
50637
+ handler: async (input, _ctx, context2) => {
50638
+ const { currency } = context2;
49831
50639
  const chartType = input.type ?? "stacked-bar";
49832
50640
  const limit = normalizeLimit(input.limit ?? 8);
49833
- const orders = await ctx.client.list("Sales Order", {
49834
- fields: ["customer_name", "status", "grand_total"],
50641
+ const orders = await context2.listAllDocuments("Sales Order", {
50642
+ fields: ["customer_name", "status", "base_grand_total"],
49835
50643
  filters: [["docstatus", "!=", 2]],
49836
- limit: 500,
49837
50644
  order_by: "modified desc"
49838
50645
  });
49839
50646
  if (chartType === "pie" || chartType === "donut") {
49840
50647
  const byCustomer = {};
49841
50648
  for (const o of orders) {
49842
50649
  const c = o.customer_name ?? "Unknown";
49843
- byCustomer[c] = (byCustomer[c] ?? 0) + (Number(o.grand_total) || 0);
50650
+ byCustomer[c] = (byCustomer[c] ?? 0) + analyticsNumber(o, "base_grand_total");
49844
50651
  }
49845
50652
  const sorted = Object.entries(byCustomer).sort(([, a], [, b]) => b - a).slice(0, limit);
49846
50653
  return {
@@ -49848,7 +50655,7 @@ var analyticsTools = [
49848
50655
  type: chartType,
49849
50656
  labels: sorted.map(([c]) => c),
49850
50657
  datasets: [{ label: "Total", values: sorted.map(([, v]) => v) }],
49851
- currency: "EUR",
50658
+ currency,
49852
50659
  _meta: CHART_META
49853
50660
  };
49854
50661
  }
@@ -49871,7 +50678,7 @@ var analyticsTools = [
49871
50678
  const c = o.customer_name ?? "Unknown";
49872
50679
  const s = o.status ?? "Draft";
49873
50680
  if (!byCustomerStatus[c]) byCustomerStatus[c] = {};
49874
- byCustomerStatus[c][s] = (byCustomerStatus[c][s] ?? 0) + (Number(o.grand_total) || 0);
50681
+ byCustomerStatus[c][s] = (byCustomerStatus[c][s] ?? 0) + analyticsNumber(o, "base_grand_total");
49875
50682
  }
49876
50683
  const customerTotals = Object.entries(byCustomerStatus).map(([c, statuses]) => ({
49877
50684
  name: c,
@@ -49891,15 +50698,15 @@ var analyticsTools = [
49891
50698
  color: STATUS_COLORS[s] ?? "#94a3b8",
49892
50699
  stack: "status"
49893
50700
  })),
49894
- currency: "EUR",
50701
+ currency,
49895
50702
  xAxisLabel: "Customer",
49896
50703
  yAxisLabel: "Order Value",
49897
50704
  _meta: CHART_META
49898
50705
  };
49899
50706
  }
49900
- },
50707
+ }),
49901
50708
  // ── Revenue vs Orders Composed ──────────────────────────────────────────
49902
- {
50709
+ companyAnalyticsTool({
49903
50710
  name: "erpnext_revenue_vs_orders",
49904
50711
  annotations: { readOnlyHint: true },
49905
50712
  _meta: CHART_META,
@@ -49915,19 +50722,19 @@ var analyticsTools = [
49915
50722
  }
49916
50723
  }
49917
50724
  },
49918
- handler: async (input, ctx) => {
50725
+ handler: async (input, _ctx, context2) => {
50726
+ const { currency } = context2;
49919
50727
  const limit = normalizeLimit(input.limit ?? 8);
49920
- const orders = await ctx.client.list("Sales Order", {
49921
- fields: ["customer_name", "grand_total"],
50728
+ const orders = await context2.listAllDocuments("Sales Order", {
50729
+ fields: ["customer_name", "base_grand_total"],
49922
50730
  filters: [["docstatus", "!=", 2]],
49923
- limit: 500,
49924
50731
  order_by: "modified desc"
49925
50732
  });
49926
50733
  const byCustomer = {};
49927
50734
  for (const o of orders) {
49928
50735
  const c = o.customer_name ?? "Unknown";
49929
50736
  if (!byCustomer[c]) byCustomer[c] = { total: 0, count: 0 };
49930
- byCustomer[c].total += Number(o.grand_total) || 0;
50737
+ byCustomer[c].total += analyticsNumber(o, "base_grand_total");
49931
50738
  byCustomer[c].count += 1;
49932
50739
  }
49933
50740
  const sorted = Object.entries(byCustomer).sort(([, a], [, b]) => b.total - a.total).slice(0, limit);
@@ -49953,15 +50760,15 @@ var analyticsTools = [
49953
50760
  }
49954
50761
  ],
49955
50762
  showRightAxis: true,
49956
- yAxisLabel: "Revenue (\u20AC)",
50763
+ yAxisLabel: `Revenue (${currency})`,
49957
50764
  rightAxisLabel: "# Orders",
49958
- currency: "EUR",
50765
+ currency,
49959
50766
  _meta: CHART_META
49960
50767
  };
49961
50768
  }
49962
- },
50769
+ }),
49963
50770
  // ── Stock Value Treemap ─────────────────────────────────────────────────
49964
- {
50771
+ companyAnalyticsTool({
49965
50772
  name: "erpnext_stock_treemap",
49966
50773
  annotations: { readOnlyHint: true },
49967
50774
  _meta: CHART_META,
@@ -49982,19 +50789,19 @@ var analyticsTools = [
49982
50789
  }
49983
50790
  }
49984
50791
  },
49985
- handler: async (input, ctx) => {
50792
+ handler: async (input, _ctx, context2) => {
50793
+ const { currency } = context2;
49986
50794
  const groupBy = input.group_by ?? "item";
49987
50795
  const limit = normalizeLimit(input.limit ?? 15);
49988
- const bins = await ctx.client.list("Bin", {
50796
+ const bins = await context2.listAllBins({
49989
50797
  fields: ["item_code", "warehouse", "stock_value"],
49990
50798
  filters: [["stock_value", ">", 0]],
49991
- limit: 500,
49992
50799
  order_by: "stock_value desc"
49993
50800
  });
49994
50801
  const grouped = {};
49995
50802
  for (const bin of bins) {
49996
50803
  const key = groupBy === "warehouse" ? bin.warehouse : bin.item_code;
49997
- grouped[key] = (grouped[key] ?? 0) + (Number(bin.stock_value) || 0);
50804
+ grouped[key] = (grouped[key] ?? 0) + analyticsNumber(bin, "stock_value");
49998
50805
  }
49999
50806
  const sorted = Object.entries(grouped).sort(([, a], [, b]) => b - a).slice(0, limit);
50000
50807
  const COLORS = [
@@ -50021,13 +50828,13 @@ var analyticsTools = [
50021
50828
  value: Math.round(value),
50022
50829
  color: COLORS[i % COLORS.length]
50023
50830
  })),
50024
- currency: "EUR",
50831
+ currency,
50025
50832
  _meta: CHART_META
50026
50833
  };
50027
50834
  }
50028
- },
50835
+ }),
50029
50836
  // ── Product Comparison Radar ────────────────────────────────────────────
50030
- {
50837
+ companyAnalyticsTool({
50031
50838
  name: "erpnext_product_radar",
50032
50839
  annotations: { readOnlyHint: true },
50033
50840
  _meta: CHART_META,
@@ -50047,7 +50854,8 @@ var analyticsTools = [
50047
50854
  }
50048
50855
  }
50049
50856
  },
50050
- handler: async (input, ctx) => {
50857
+ handler: async (input, _ctx, context2) => {
50858
+ const { currency } = context2;
50051
50859
  let itemCodes = input.items ?? [];
50052
50860
  if (itemCodes.length > MAX_RADAR_ITEMS) {
50053
50861
  throw new Error(
@@ -50055,7 +50863,7 @@ var analyticsTools = [
50055
50863
  );
50056
50864
  }
50057
50865
  if (itemCodes.length === 0) {
50058
- const topBins = await ctx.client.list("Bin", {
50866
+ const topBins = await context2.listBins({
50059
50867
  fields: ["item_code"],
50060
50868
  filters: [["actual_qty", ">", 0]],
50061
50869
  limit: 4,
@@ -50072,40 +50880,43 @@ var analyticsTools = [
50072
50880
  _meta: CHART_META
50073
50881
  };
50074
50882
  }
50075
- const dimensions = ["Stock Qty", "Stock Value", "Order Lines", "Revenue"];
50883
+ const dimensions = [
50884
+ "Stock Qty",
50885
+ `Stock Value (${currency})`,
50886
+ "Order Lines",
50887
+ `Revenue (${currency})`
50888
+ ];
50076
50889
  const raw2 = {};
50077
50890
  const binResults = await Promise.all(
50078
50891
  itemCodes.map(
50079
- (code) => ctx.client.list("Bin", {
50892
+ (code) => context2.listAllBins({
50080
50893
  fields: ["actual_qty", "stock_value"],
50081
- filters: [["item_code", "=", code]],
50082
- limit: 100
50894
+ filters: [["item_code", "=", code]]
50083
50895
  })
50084
50896
  )
50085
50897
  );
50086
50898
  itemCodes.forEach((code, i) => {
50087
50899
  const bins = binResults[i];
50088
50900
  const totalQty = bins.reduce(
50089
- (s, b) => s + (Number(b.actual_qty) || 0),
50901
+ (s, b) => s + analyticsNumber(b, "actual_qty"),
50090
50902
  0
50091
50903
  );
50092
50904
  const totalVal = bins.reduce(
50093
- (s, b) => s + (Number(b.stock_value) || 0),
50905
+ (s, b) => s + analyticsNumber(b, "stock_value"),
50094
50906
  0
50095
50907
  );
50096
50908
  raw2[code] = [totalQty, totalVal, 0, 0];
50097
50909
  });
50098
- const soItems = await ctx.client.list("Sales Order Item", {
50099
- fields: ["item_code", "qty", "amount"],
50100
- filters: [["docstatus", "!=", 2]],
50101
- limit: 500
50910
+ const soItems = await context2.listAllItems("Sales Order", {
50911
+ fields: ["item_code", "qty", "base_amount"],
50912
+ filters: [["docstatus", "!=", 2]]
50102
50913
  });
50103
50914
  const itemSet = new Set(itemCodes);
50104
50915
  for (const row of soItems) {
50105
50916
  const code = row.item_code;
50106
50917
  if (itemSet.has(code) && raw2[code]) {
50107
50918
  raw2[code][2] += 1;
50108
- raw2[code][3] += Number(row.amount) || 0;
50919
+ raw2[code][3] += analyticsNumber(row, "base_amount");
50109
50920
  }
50110
50921
  }
50111
50922
  const maxPerDim = dimensions.map(
@@ -50127,9 +50938,9 @@ var analyticsTools = [
50127
50938
  _meta: CHART_META
50128
50939
  };
50129
50940
  }
50130
- },
50941
+ }),
50131
50942
  // ── Price vs Quantity Scatter ────────────────────────────────────────────
50132
- {
50943
+ companyAnalyticsTool({
50133
50944
  name: "erpnext_price_vs_qty",
50134
50945
  annotations: { readOnlyHint: true },
50135
50946
  _meta: CHART_META,
@@ -50145,33 +50956,36 @@ var analyticsTools = [
50145
50956
  }
50146
50957
  }
50147
50958
  },
50148
- handler: async (input, ctx) => {
50959
+ handler: async (input, ctx, context2) => {
50960
+ const { currency } = context2;
50149
50961
  const limit = normalizeLimit(input.limit ?? 30);
50150
- const items = await ctx.client.list("Item Price", {
50151
- fields: ["item_code", "price_list_rate"],
50152
- filters: [["selling", "=", 1]],
50153
- limit: 200,
50154
- order_by: "modified desc"
50155
- });
50962
+ const items = await listCompleteAnalytics(
50963
+ ctx.client.list.bind(ctx.client),
50964
+ "Item Price",
50965
+ {
50966
+ fields: ["item_code", "price_list_rate", "currency", "uom"],
50967
+ filters: [["selling", "=", 1]],
50968
+ order_by: "modified desc"
50969
+ }
50970
+ );
50156
50971
  const priceMap = {};
50157
50972
  for (const ip of items) {
50158
50973
  const code = ip.item_code;
50159
- if (!priceMap[code]) priceMap[code] = Number(ip.price_list_rate) || 0;
50974
+ if (!Object.hasOwn(priceMap, code)) priceMap[code] = ip;
50160
50975
  }
50161
- const soItems = await ctx.client.list("Sales Order Item", {
50162
- fields: ["item_code", "qty"],
50163
- filters: [["docstatus", "!=", 2]],
50164
- limit: 500
50976
+ const soItems = await context2.listAllItems("Sales Order", {
50977
+ fields: ["item_code", "stock_qty"],
50978
+ filters: [["docstatus", "!=", 2]]
50165
50979
  });
50166
50980
  const qtyMap = {};
50167
50981
  for (const row of soItems) {
50168
50982
  const code = row.item_code;
50169
- qtyMap[code] = (qtyMap[code] ?? 0) + (Number(row.qty) || 0);
50983
+ qtyMap[code] = (qtyMap[code] ?? 0) + analyticsNumber(row, "stock_qty");
50170
50984
  }
50171
50985
  const allItems = Object.keys(priceMap).filter((c) => qtyMap[c] != null);
50172
50986
  const limited = allItems.slice(0, limit);
50173
50987
  if (limited.length === 0) {
50174
- const bins = await ctx.client.list("Bin", {
50988
+ const bins = await context2.listBins({
50175
50989
  fields: ["item_code", "valuation_rate", "actual_qty"],
50176
50990
  filters: [["actual_qty", ">", 0], ["valuation_rate", ">", 0]],
50177
50991
  limit,
@@ -50186,16 +51000,34 @@ var analyticsTools = [
50186
51000
  label: "Items",
50187
51001
  color: "#818cf8",
50188
51002
  points: bins.map((b) => ({
50189
- x: Math.round(Number(b.valuation_rate) || 0),
51003
+ x: Math.round(analyticsNumber(b, "valuation_rate")),
50190
51004
  y: Math.round(Number(b.actual_qty) || 0),
50191
51005
  label: b.item_code
50192
51006
  }))
50193
51007
  }],
50194
- xAxisLabel: "Valuation Rate (\u20AC/unit)",
51008
+ xAxisLabel: `Valuation Rate (${currency}/stock unit)`,
50195
51009
  yAxisLabel: "Stock Qty",
50196
51010
  _meta: CHART_META
50197
51011
  };
50198
51012
  }
51013
+ const itemDocs = await listAnalyticsItemUnits(ctx, limited);
51014
+ const stockUnits = new Map(
51015
+ itemDocs.map((item) => [item.name, item.stock_uom])
51016
+ );
51017
+ for (const code of limited) {
51018
+ const price = priceMap[code];
51019
+ if (price.currency !== currency) {
51020
+ throw new Error(
51021
+ `Item Price for '${code}' has missing or unexpected currency; expected ${currency}. Historical conversion is unavailable.`
51022
+ );
51023
+ }
51024
+ const stockUom = stockUnits.get(code);
51025
+ if (typeof stockUom !== "string" || stockUom === "" || price.uom !== stockUom) {
51026
+ throw new Error(
51027
+ `Item Price for '${code}' must use its verified stock UOM; conversion is unavailable.`
51028
+ );
51029
+ }
51030
+ }
50199
51031
  return {
50200
51032
  title: "Price vs Quantity Ordered",
50201
51033
  type: "scatter",
@@ -50205,44 +51037,44 @@ var analyticsTools = [
50205
51037
  label: "Items",
50206
51038
  color: "#818cf8",
50207
51039
  points: limited.map((code) => ({
50208
- x: Math.round(priceMap[code]),
51040
+ x: Math.round(analyticsNumber(priceMap[code], "price_list_rate")),
50209
51041
  y: Math.round(qtyMap[code]),
50210
51042
  label: code
50211
51043
  }))
50212
51044
  }],
50213
- xAxisLabel: "Selling Price (\u20AC)",
50214
- yAxisLabel: "Total Qty Ordered",
51045
+ xAxisLabel: `Selling Price (${currency}/stock unit)`,
51046
+ yAxisLabel: "Total Stock Qty Ordered",
50215
51047
  _meta: CHART_META
50216
51048
  };
50217
51049
  }
50218
- },
51050
+ }),
50219
51051
  // ── KPI: Revenue MTD ────────────────────────────────────────────────────
50220
- {
51052
+ companyAnalyticsTool({
50221
51053
  name: "erpnext_kpi_revenue",
50222
51054
  annotations: { readOnlyHint: true },
50223
51055
  _meta: KPI_META,
50224
51056
  description: "KPI card: total Sales Order revenue for the current month, with delta % vs previous month and sparkline of last 6 months.",
50225
51057
  category: "analytics",
50226
51058
  inputSchema: { type: "object", properties: {} },
50227
- handler: async (_input, ctx) => {
50228
- const now = /* @__PURE__ */ new Date();
50229
- const sixMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 5, 1);
50230
- const sinceStr = sixMonthsAgo.toISOString().split("T")[0];
50231
- const allOrders = await ctx.client.list("Sales Order", {
50232
- fields: ["grand_total", "transaction_date"],
51059
+ handler: async (_input, ctx, context2) => {
51060
+ const { currency } = context2;
51061
+ const window2 = analyticsDateWindow(await siteToday(ctx), 6);
51062
+ const allOrders = await context2.listAllDocuments("Sales Order", {
51063
+ fields: ["base_grand_total", "transaction_date"],
50233
51064
  filters: [
50234
- ["transaction_date", ">=", sinceStr],
51065
+ ["transaction_date", ">=", window2.start],
51066
+ ["transaction_date", "<=", window2.end],
50235
51067
  ["docstatus", "!=", 2]
50236
- ],
50237
- limit: 5e3
51068
+ ]
50238
51069
  });
50239
51070
  const sparkline = [0, 0, 0, 0, 0, 0];
50240
51071
  for (const o of allOrders) {
50241
- const d = new Date(o.transaction_date);
50242
- const monthDiff = (now.getFullYear() - d.getFullYear()) * 12 + (now.getMonth() - d.getMonth());
50243
- const idx = 5 - monthDiff;
51072
+ const idx = analyticsMonthIndex(
51073
+ o.transaction_date,
51074
+ window2.monthKeys
51075
+ );
50244
51076
  if (idx >= 0 && idx < 6) {
50245
- sparkline[idx] += Number(o.grand_total) || 0;
51077
+ sparkline[idx] += analyticsNumber(o, "base_grand_total");
50246
51078
  }
50247
51079
  }
50248
51080
  const currentTotal = sparkline[5];
@@ -50251,7 +51083,7 @@ var analyticsTools = [
50251
51083
  return {
50252
51084
  label: "Revenue MTD",
50253
51085
  value: currentTotal,
50254
- currency: "EUR",
51086
+ currency,
50255
51087
  delta: Math.round(delta * 10) / 10,
50256
51088
  deltaLabel: "vs last month",
50257
51089
  trend: delta > 0 ? "up" : delta < 0 ? "down" : "flat",
@@ -50261,74 +51093,77 @@ var analyticsTools = [
50261
51093
  _meta: KPI_META
50262
51094
  };
50263
51095
  }
50264
- },
51096
+ }),
50265
51097
  // ── KPI: Outstanding Receivables ────────────────────────────────────────
50266
- {
51098
+ companyAnalyticsTool({
50267
51099
  name: "erpnext_kpi_outstanding",
50268
51100
  annotations: { readOnlyHint: true },
50269
51101
  _meta: KPI_META,
50270
- description: "KPI card: total outstanding receivables from submitted Sales Invoices with outstanding_amount > 0. Shows count of open invoices.",
51102
+ description: "KPI card: total outstanding receivables from submitted Sales Invoices with positive outstanding balance from Accounts Receivable in company currency. Shows count of open invoices.",
50271
51103
  category: "analytics",
50272
51104
  inputSchema: { type: "object", properties: {} },
50273
- handler: async (_input, ctx) => {
50274
- const invoices = await ctx.client.list("Sales Invoice", {
50275
- fields: ["outstanding_amount"],
50276
- filters: [
50277
- ["outstanding_amount", ">", 0],
50278
- ["docstatus", "=", 1]
50279
- ],
50280
- limit: 1e3
50281
- });
51105
+ handler: async (_input, ctx, context2) => {
51106
+ const { currency } = context2;
51107
+ const reportDate = await siteToday(ctx);
51108
+ const reportRows = await receivableInvoiceRows(
51109
+ ctx,
51110
+ context2.company,
51111
+ currency,
51112
+ reportDate
51113
+ );
51114
+ const invoices = reportRows;
50282
51115
  const total = invoices.reduce(
50283
- (sum, inv) => sum + (Number(inv.outstanding_amount) || 0),
51116
+ (sum, inv) => sum + inv.outstanding_amount,
50284
51117
  0
50285
51118
  );
50286
- const count = invoices.length;
51119
+ const count = new Set(invoices.map((row) => row.voucher_no)).size;
50287
51120
  return {
50288
51121
  label: "Outstanding Receivables",
50289
51122
  value: total,
50290
- formattedValue: `${count} inv. / ${total.toLocaleString("en-US", { style: "currency", currency: "EUR" })}`,
50291
- currency: "EUR",
51123
+ formattedValue: `${count} inv. / ${total.toLocaleString("en-US", { style: "currency", currency })}`,
51124
+ currency,
50292
51125
  trend: total > 0 ? "up" : "flat",
50293
51126
  trendIsGood: false,
50294
51127
  color: "#fbbf24",
50295
51128
  _meta: KPI_META
50296
51129
  };
50297
51130
  }
50298
- },
51131
+ }),
50299
51132
  // ── KPI: Orders This Month ──────────────────────────────────────────────
50300
51133
  {
50301
51134
  name: "erpnext_kpi_orders",
50302
51135
  annotations: { readOnlyHint: true },
50303
51136
  _meta: KPI_META,
50304
- description: "KPI card: count and total value of Sales Orders created this month, with delta % vs last month.",
51137
+ description: "KPI card: count of Sales Orders created this month, with delta % vs last month.",
50305
51138
  category: "analytics",
50306
51139
  inputSchema: { type: "object", properties: {} },
50307
51140
  handler: async (_input, ctx) => {
50308
- const now = /* @__PURE__ */ new Date();
50309
- const thisMonthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
50310
- const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
50311
- const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
50312
- const lastMonthStartStr = lastMonthStart.toISOString().split("T")[0];
50313
- const lastMonthEndStr = lastMonthEnd.toISOString().split("T")[0];
50314
- const currentOrders = await ctx.client.list("Sales Order", {
50315
- fields: ["grand_total"],
50316
- filters: [
50317
- ["transaction_date", ">=", thisMonthStart],
50318
- ["docstatus", "!=", 2]
50319
- ],
50320
- limit: 1e3
50321
- });
51141
+ const window2 = analyticsDateWindow(await siteToday(ctx));
51142
+ const currentOrders = await listCompleteAnalytics(
51143
+ ctx.client.list.bind(ctx.client),
51144
+ "Sales Order",
51145
+ {
51146
+ fields: ["name"],
51147
+ filters: [
51148
+ ["transaction_date", ">=", window2.start],
51149
+ ["transaction_date", "<=", window2.end],
51150
+ ["docstatus", "!=", 2]
51151
+ ]
51152
+ }
51153
+ );
50322
51154
  const currentCount = currentOrders.length;
50323
- const prevOrders = await ctx.client.list("Sales Order", {
50324
- fields: ["grand_total"],
50325
- filters: [
50326
- ["transaction_date", ">=", lastMonthStartStr],
50327
- ["transaction_date", "<=", lastMonthEndStr],
50328
- ["docstatus", "!=", 2]
50329
- ],
50330
- limit: 1e3
50331
- });
51155
+ const prevOrders = await listCompleteAnalytics(
51156
+ ctx.client.list.bind(ctx.client),
51157
+ "Sales Order",
51158
+ {
51159
+ fields: ["name"],
51160
+ filters: [
51161
+ ["transaction_date", ">=", window2.previousStart],
51162
+ ["transaction_date", "<=", window2.previousEnd],
51163
+ ["docstatus", "!=", 2]
51164
+ ]
51165
+ }
51166
+ );
50332
51167
  const prevCount = prevOrders.length;
50333
51168
  const delta = prevCount > 0 ? (currentCount - prevCount) / prevCount * 100 : 0;
50334
51169
  return {
@@ -50346,47 +51181,46 @@ var analyticsTools = [
50346
51181
  }
50347
51182
  },
50348
51183
  // ── KPI: Gross Margin ──────────────────────────────────────────────────
50349
- {
51184
+ companyAnalyticsTool({
50350
51185
  name: "erpnext_kpi_gross_margin",
50351
51186
  annotations: { readOnlyHint: true },
50352
51187
  _meta: KPI_META,
50353
51188
  description: "KPI card: estimated gross margin % based on Sales Order revenue vs valuation rate from stock (Bin). Margin = (revenue - cost) / revenue * 100.",
50354
51189
  category: "analytics",
50355
51190
  inputSchema: { type: "object", properties: {} },
50356
- handler: async (_input, ctx) => {
50357
- const soItems = await ctx.client.list("Sales Order Item", {
50358
- fields: ["item_code", "qty", "amount"],
51191
+ handler: async (_input, _ctx, context2) => {
51192
+ const soItems = await context2.listAllItems("Sales Order", {
51193
+ fields: ["item_code", "stock_qty", "base_amount"],
50359
51194
  filters: [
50360
51195
  ["docstatus", "!=", 2]
50361
- ],
50362
- limit: 1e3
51196
+ ]
50363
51197
  });
50364
51198
  const revenue = soItems.reduce(
50365
- (sum, row) => sum + (Number(row.amount) || 0),
51199
+ (sum, row) => sum + analyticsNumber(row, "base_amount"),
50366
51200
  0
50367
51201
  );
50368
51202
  const itemQty = {};
50369
51203
  for (const row of soItems) {
50370
51204
  const code = row.item_code;
50371
- itemQty[code] = (itemQty[code] ?? 0) + (Number(row.qty) || 0);
51205
+ itemQty[code] = (itemQty[code] ?? 0) + analyticsNumber(row, "stock_qty");
50372
51206
  }
50373
- const bins = await ctx.client.list("Bin", {
51207
+ const bins = await context2.listAllBins({
50374
51208
  fields: ["item_code", "valuation_rate"],
50375
- filters: [["valuation_rate", ">", 0]],
50376
- limit: 500
51209
+ filters: [["valuation_rate", ">", 0]]
50377
51210
  });
50378
51211
  const valMap = {};
50379
51212
  for (const bin of bins) {
50380
51213
  const code = bin.item_code;
50381
51214
  if (!valMap[code]) {
50382
- valMap[code] = Number(bin.valuation_rate) || 0;
51215
+ valMap[code] = analyticsNumber(bin, "valuation_rate");
50383
51216
  }
50384
51217
  }
50385
51218
  let cost = 0;
50386
51219
  for (const [code, qty] of Object.entries(itemQty)) {
50387
- if (valMap[code]) {
50388
- cost += valMap[code] * qty;
51220
+ if (valMap[code] === void 0) {
51221
+ throw new Error(`Missing estimated stock cost for item '${code}'.`);
50389
51222
  }
51223
+ cost += valMap[code] * qty;
50390
51224
  }
50391
51225
  const margin = revenue > 0 ? (revenue - cost) / revenue * 100 : 0;
50392
51226
  return {
@@ -50399,48 +51233,47 @@ var analyticsTools = [
50399
51233
  _meta: KPI_META
50400
51234
  };
50401
51235
  }
50402
- },
51236
+ }),
50403
51237
  // ── KPI: Overdue Invoices ──────────────────────────────────────────────
50404
- {
51238
+ companyAnalyticsTool({
50405
51239
  name: "erpnext_kpi_overdue",
50406
51240
  annotations: { readOnlyHint: true },
50407
51241
  _meta: KPI_META,
50408
51242
  description: "KPI card: count and total value of overdue Sales Invoices (due_date < today, outstanding_amount > 0, submitted).",
50409
51243
  category: "analytics",
50410
51244
  inputSchema: { type: "object", properties: {} },
50411
- handler: async (_input, ctx) => {
50412
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
50413
- const invoices = await ctx.client.list("Sales Invoice", {
50414
- fields: ["outstanding_amount", "due_date"],
50415
- filters: [
50416
- ["due_date", "<", today],
50417
- ["outstanding_amount", ">", 0],
50418
- ["docstatus", "=", 1]
50419
- ],
50420
- limit: 1e3
50421
- });
50422
- const count = invoices.length;
51245
+ handler: async (_input, ctx, context2) => {
51246
+ const { currency } = context2;
51247
+ const reportDate = await siteToday(ctx);
51248
+ const reportRows = await receivableInvoiceRows(
51249
+ ctx,
51250
+ context2.company,
51251
+ currency,
51252
+ reportDate
51253
+ );
51254
+ const invoices = reportRows.filter((row) => row.due_date < reportDate);
51255
+ const count = new Set(invoices.map((row) => row.voucher_no)).size;
50423
51256
  const total = invoices.reduce(
50424
- (sum, inv) => sum + (Number(inv.outstanding_amount) || 0),
51257
+ (sum, inv) => sum + inv.outstanding_amount,
50425
51258
  0
50426
51259
  );
50427
51260
  return {
50428
51261
  label: "Overdue Invoices",
50429
51262
  value: count,
50430
- formattedValue: `${count} inv. / ${total.toLocaleString("en-US", { style: "currency", currency: "EUR" })}`,
51263
+ formattedValue: `${count} inv. / ${total.toLocaleString("en-US", { style: "currency", currency })}`,
50431
51264
  trend: count > 0 ? "up" : "flat",
50432
51265
  trendIsGood: false,
50433
51266
  color: "#f87171",
50434
51267
  _meta: KPI_META
50435
51268
  };
50436
51269
  }
50437
- },
51270
+ }),
50438
51271
  // ── Sales Funnel ──────────────────────────────────────────────────────────
50439
- {
51272
+ companyAnalyticsTool({
50440
51273
  name: "erpnext_sales_funnel",
50441
51274
  annotations: { readOnlyHint: true },
50442
51275
  _meta: FUNNEL_META,
50443
- description: "Sales funnel from Lead \u2192 Opportunity \u2192 Quotation \u2192 Sales Order. Shows count and value at each stage with conversion rates between stages.",
51276
+ description: "Sales funnel from Lead \u2192 Opportunity \u2192 Quotation \u2192 Sales Order. Shows count and value at each stage with conversion rates between stages. Opportunity, Quotation and Order are scoped to one company; Leads count all visible records.",
50444
51277
  category: "analytics",
50445
51278
  inputSchema: {
50446
51279
  type: "object",
@@ -50452,44 +51285,44 @@ var analyticsTools = [
50452
51285
  }
50453
51286
  }
50454
51287
  },
50455
- handler: async (_input, ctx) => {
51288
+ handler: async (_input, ctx, context2) => {
51289
+ const { currency } = context2;
50456
51290
  const period = _input.period ?? "all";
50457
- const now = /* @__PURE__ */ new Date();
50458
- let sinceDate = null;
50459
- if (period === "this_month") {
50460
- sinceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
50461
- } else if (period === "this_quarter") {
50462
- const qMonth = Math.floor(now.getMonth() / 3) * 3 + 1;
50463
- sinceDate = `${now.getFullYear()}-${String(qMonth).padStart(2, "0")}-01`;
50464
- } else if (period === "this_year") {
50465
- sinceDate = `${now.getFullYear()}-01-01`;
50466
- }
50467
- const leadFilters = sinceDate ? [["creation", ">=", sinceDate]] : [];
50468
- const txnFilters = sinceDate ? [["transaction_date", ">=", sinceDate]] : [];
51291
+ const finitePeriod = ["this_month", "this_quarter", "this_year"].includes(
51292
+ period
51293
+ );
51294
+ const window2 = finitePeriod ? analyticsDateWindow(await siteToday(ctx)) : null;
51295
+ const sinceDate = window2 ? period === "this_quarter" ? window2.quarterStart : period === "this_year" ? window2.yearStart : window2.start : null;
51296
+ const leadFilters = sinceDate && window2 ? [["creation", ">=", `${sinceDate} 00:00:00`], [
51297
+ "creation",
51298
+ "<",
51299
+ `${window2.nextDay} 00:00:00`
51300
+ ]] : [];
51301
+ const txnFilters = sinceDate && window2 ? [["transaction_date", ">=", sinceDate], [
51302
+ "transaction_date",
51303
+ "<=",
51304
+ window2.end
51305
+ ]] : [];
50469
51306
  const submittedTxnFilters = [
50470
51307
  ...txnFilters,
50471
51308
  ["docstatus", "!=", 2]
50472
51309
  ];
50473
51310
  const [leads, opps, quots, orders] = await Promise.all([
50474
- ctx.client.list("Lead", {
51311
+ listCompleteAnalytics(ctx.client.list.bind(ctx.client), "Lead", {
50475
51312
  fields: ["name"],
50476
- filters: leadFilters,
50477
- limit: 500
51313
+ filters: leadFilters
50478
51314
  }),
50479
- ctx.client.list("Opportunity", {
50480
- fields: ["name", "opportunity_amount"],
50481
- filters: txnFilters,
50482
- limit: 500
51315
+ context2.listAllDocuments("Opportunity", {
51316
+ fields: ["name", "base_opportunity_amount"],
51317
+ filters: txnFilters
50483
51318
  }),
50484
- ctx.client.list("Quotation", {
50485
- fields: ["name", "grand_total"],
50486
- filters: submittedTxnFilters,
50487
- limit: 500
51319
+ context2.listAllDocuments("Quotation", {
51320
+ fields: ["name", "base_grand_total"],
51321
+ filters: submittedTxnFilters
50488
51322
  }),
50489
- ctx.client.list("Sales Order", {
50490
- fields: ["name", "grand_total"],
50491
- filters: submittedTxnFilters,
50492
- limit: 500
51323
+ context2.listAllDocuments("Sales Order", {
51324
+ fields: ["name", "base_grand_total"],
51325
+ filters: submittedTxnFilters
50493
51326
  })
50494
51327
  ]);
50495
51328
  const stages = [
@@ -50502,7 +51335,7 @@ var analyticsTools = [
50502
51335
  label: "Opportunities",
50503
51336
  count: opps.length,
50504
51337
  value: opps.reduce(
50505
- (s, o) => s + (Number(o.opportunity_amount) || 0),
51338
+ (s, o) => s + analyticsNumber(o, "base_opportunity_amount"),
50506
51339
  0
50507
51340
  ),
50508
51341
  color: "#60a5fa",
@@ -50511,14 +51344,20 @@ var analyticsTools = [
50511
51344
  {
50512
51345
  label: "Quotations",
50513
51346
  count: quots.length,
50514
- value: quots.reduce((s, q) => s + (Number(q.grand_total) || 0), 0),
51347
+ value: quots.reduce(
51348
+ (s, q) => s + analyticsNumber(q, "base_grand_total"),
51349
+ 0
51350
+ ),
50515
51351
  color: "#4ade80",
50516
51352
  conversionRate: opps.length > 0 ? Math.round(quots.length / opps.length * 100) : 0
50517
51353
  },
50518
51354
  {
50519
51355
  label: "Orders",
50520
51356
  count: orders.length,
50521
- value: orders.reduce((s, o) => s + (Number(o.grand_total) || 0), 0),
51357
+ value: orders.reduce(
51358
+ (s, o) => s + analyticsNumber(o, "base_grand_total"),
51359
+ 0
51360
+ ),
50522
51361
  color: "#fbbf24",
50523
51362
  conversionRate: quots.length > 0 ? Math.round(orders.length / quots.length * 100) : 0
50524
51363
  }
@@ -50531,15 +51370,15 @@ var analyticsTools = [
50531
51370
  };
50532
51371
  return {
50533
51372
  title: "Sales Funnel",
50534
- subtitle: periodLabels[period] ?? "All Time",
51373
+ subtitle: `${periodLabels[period] ?? "All Time"}; ${context2.company}; Leads: all visible companies`,
50535
51374
  stages,
50536
- currency: "EUR",
51375
+ currency,
50537
51376
  _meta: FUNNEL_META
50538
51377
  };
50539
51378
  }
50540
- },
51379
+ }),
50541
51380
  // ── AR Aging ──────────────────────────────────────────────────────────────
50542
- {
51381
+ companyAnalyticsTool({
50543
51382
  name: "erpnext_ar_aging",
50544
51383
  annotations: { readOnlyHint: true },
50545
51384
  _meta: CHART_META,
@@ -50560,21 +51399,19 @@ var analyticsTools = [
50560
51399
  }
50561
51400
  }
50562
51401
  },
50563
- handler: async (input, ctx) => {
51402
+ handler: async (input, ctx, context2) => {
51403
+ const { currency } = context2;
50564
51404
  const limit = normalizeLimit(input.limit ?? 10);
50565
51405
  const chartType = input.type ?? "stacked-bar";
50566
- const invoices = await ctx.client.list("Sales Invoice", {
50567
- fields: [
50568
- "customer_name",
50569
- "outstanding_amount",
50570
- "due_date",
50571
- "posting_date"
50572
- ],
50573
- filters: [["outstanding_amount", ">", 0], ["docstatus", "=", 1]],
50574
- limit: 500,
50575
- order_by: "outstanding_amount desc"
50576
- });
50577
- const today = /* @__PURE__ */ new Date();
51406
+ const reportDate = await siteToday(ctx);
51407
+ const reportRows = await receivableInvoiceRows(
51408
+ ctx,
51409
+ context2.company,
51410
+ currency,
51411
+ reportDate
51412
+ );
51413
+ const invoices = reportRows;
51414
+ const today = new Date(reportDate);
50578
51415
  const BUCKETS = [
50579
51416
  { label: "0-30 days", min: 0, max: 30, color: "#4ade80" },
50580
51417
  { label: "31-60 days", min: 31, max: 60, color: "#fbbf24" },
@@ -50597,7 +51434,7 @@ var analyticsTools = [
50597
51434
  (b) => agingDays >= b.min && agingDays <= b.max
50598
51435
  );
50599
51436
  if (bucketIdx >= 0) {
50600
- byCustomer[customer][bucketIdx] += Number(inv.outstanding_amount) || 0;
51437
+ byCustomer[customer][bucketIdx] += inv.outstanding_amount;
50601
51438
  }
50602
51439
  }
50603
51440
  const sorted = Object.entries(byCustomer).map(([name, buckets]) => ({
@@ -50628,7 +51465,7 @@ var analyticsTools = [
50628
51465
  value: Math.round(total),
50629
51466
  color: COLORS[i % COLORS.length]
50630
51467
  })),
50631
- currency: "EUR",
51468
+ currency,
50632
51469
  _meta: CHART_META
50633
51470
  };
50634
51471
  }
@@ -50644,15 +51481,15 @@ var analyticsTools = [
50644
51481
  color: bucket.color,
50645
51482
  stack: "aging"
50646
51483
  })),
50647
- currency: "EUR",
51484
+ currency,
50648
51485
  xAxisLabel: "Customer",
50649
51486
  yAxisLabel: "Outstanding Amount",
50650
51487
  _meta: CHART_META
50651
51488
  };
50652
51489
  }
50653
- },
51490
+ }),
50654
51491
  // ── Gross Profit ──────────────────────────────────────────────────────────
50655
- {
51492
+ companyAnalyticsTool({
50656
51493
  name: "erpnext_gross_profit",
50657
51494
  annotations: { readOnlyHint: true },
50658
51495
  _meta: CHART_META,
@@ -50673,29 +51510,33 @@ var analyticsTools = [
50673
51510
  }
50674
51511
  }
50675
51512
  },
50676
- handler: async (input, ctx) => {
51513
+ handler: async (input, _ctx, context2) => {
51514
+ const { currency } = context2;
50677
51515
  const limit = normalizeLimit(input.limit ?? 10);
50678
51516
  const groupBy = input.group_by ?? "item";
50679
51517
  const needsCustomers = groupBy === "customer";
50680
51518
  const [siItems, bins, invoices] = await Promise.all([
50681
51519
  // Submitted Sales Invoice Items for revenue
50682
- ctx.client.list("Sales Invoice Item", {
50683
- fields: ["parent", "item_code", "item_name", "amount", "qty"],
51520
+ context2.listAllItems("Sales Invoice", {
51521
+ fields: [
51522
+ "parent",
51523
+ "item_code",
51524
+ "item_name",
51525
+ "base_amount",
51526
+ "stock_qty"
51527
+ ],
50684
51528
  filters: [["docstatus", "=", 1]],
50685
- limit: 500,
50686
- order_by: "amount desc"
51529
+ order_by: "base_amount desc"
50687
51530
  }),
50688
51531
  // Bin for valuation_rate (cost per unit)
50689
- ctx.client.list("Bin", {
51532
+ context2.listAllBins({
50690
51533
  fields: ["item_code", "valuation_rate"],
50691
- filters: [["valuation_rate", ">", 0]],
50692
- limit: 500
51534
+ filters: [["valuation_rate", ">", 0]]
50693
51535
  }),
50694
51536
  // Invoices, to map parent invoice name to customer
50695
- needsCustomers ? ctx.client.list("Sales Invoice", {
51537
+ needsCustomers ? context2.listAllDocuments("Sales Invoice", {
50696
51538
  fields: ["name", "customer_name"],
50697
- filters: [["docstatus", "=", 1]],
50698
- limit: 500
51539
+ filters: [["docstatus", "=", 1]]
50699
51540
  }) : Promise.resolve([])
50700
51541
  ]);
50701
51542
  const costMap = {};
@@ -50703,7 +51544,7 @@ var analyticsTools = [
50703
51544
  const code = bin.item_code;
50704
51545
  costMap[code] = Math.max(
50705
51546
  costMap[code] ?? 0,
50706
- Number(bin.valuation_rate) || 0
51547
+ analyticsNumber(bin, "valuation_rate")
50707
51548
  );
50708
51549
  }
50709
51550
  if (needsCustomers) {
@@ -50717,9 +51558,14 @@ var analyticsTools = [
50717
51558
  if (!byCustomer[customer]) {
50718
51559
  byCustomer[customer] = { revenue: 0, cost: 0 };
50719
51560
  }
50720
- const qty = Number(row.qty) || 0;
50721
- const unitCost = costMap[row.item_code] ?? 0;
50722
- byCustomer[customer].revenue += Number(row.amount) || 0;
51561
+ const qty = analyticsNumber(row, "stock_qty");
51562
+ const unitCost = costMap[row.item_code];
51563
+ if (unitCost === void 0) {
51564
+ throw new Error(
51565
+ `Missing estimated stock cost for item '${row.item_code}'.`
51566
+ );
51567
+ }
51568
+ byCustomer[customer].revenue += analyticsNumber(row, "base_amount");
50723
51569
  byCustomer[customer].cost += qty * unitCost;
50724
51570
  }
50725
51571
  const sorted2 = Object.entries(byCustomer).sort(([, a], [, b]) => b.revenue - a.revenue).slice(0, limit);
@@ -50730,7 +51576,7 @@ var analyticsTools = [
50730
51576
  );
50731
51577
  return {
50732
51578
  title: "Gross Profit by Customer",
50733
- subtitle: `Top ${labels2.length} customers`,
51579
+ subtitle: `Top ${labels2.length} customers; estimated cost from current stock valuation`,
50734
51580
  type: "composed",
50735
51581
  labels: labels2,
50736
51582
  datasets: [
@@ -50750,7 +51596,7 @@ var analyticsTools = [
50750
51596
  }
50751
51597
  ],
50752
51598
  showRightAxis: true,
50753
- currency: "EUR",
51599
+ currency,
50754
51600
  yAxisLabel: "Revenue",
50755
51601
  rightAxisLabel: "Margin %",
50756
51602
  _meta: CHART_META
@@ -50766,9 +51612,12 @@ var analyticsTools = [
50766
51612
  cost: 0
50767
51613
  };
50768
51614
  }
50769
- const qty = Number(row.qty) || 0;
50770
- const unitCost = costMap[code] ?? 0;
50771
- byItem[code].revenue += Number(row.amount) || 0;
51615
+ const qty = analyticsNumber(row, "stock_qty");
51616
+ const unitCost = costMap[code];
51617
+ if (unitCost === void 0) {
51618
+ throw new Error(`Missing estimated stock cost for item '${code}'.`);
51619
+ }
51620
+ byItem[code].revenue += analyticsNumber(row, "base_amount");
50772
51621
  byItem[code].cost += qty * unitCost;
50773
51622
  }
50774
51623
  const sorted = Object.entries(byItem).sort(([, a], [, b]) => b.revenue - a.revenue).slice(0, limit);
@@ -50779,7 +51628,7 @@ var analyticsTools = [
50779
51628
  );
50780
51629
  return {
50781
51630
  title: "Gross Profit by Item",
50782
- subtitle: `Top ${labels.length} items`,
51631
+ subtitle: `Top ${labels.length} items; estimated cost from current stock valuation`,
50783
51632
  type: "composed",
50784
51633
  labels,
50785
51634
  datasets: [
@@ -50799,13 +51648,13 @@ var analyticsTools = [
50799
51648
  }
50800
51649
  ],
50801
51650
  showRightAxis: true,
50802
- currency: "EUR",
51651
+ currency,
50803
51652
  yAxisLabel: "Revenue",
50804
51653
  rightAxisLabel: "Margin %",
50805
51654
  _meta: CHART_META
50806
51655
  };
50807
51656
  }
50808
- },
51657
+ }),
50809
51658
  // ── Profit & Loss ─────────────────────────────────────────────────────────
50810
51659
  {
50811
51660
  name: "erpnext_profit_loss",
@@ -50977,6 +51826,10 @@ var analyticsTools = [
50977
51826
  periodicity: "Monthly",
50978
51827
  cross_check: crossCheck
50979
51828
  },
51829
+ refreshRequest: {
51830
+ toolName: "erpnext_profit_loss",
51831
+ arguments: { ...input, company }
51832
+ },
50980
51833
  _meta: CHART_META
50981
51834
  };
50982
51835
  }
@@ -51268,7 +52121,9 @@ var taskKanbanAdapter = {
51268
52121
  return { allowed: true };
51269
52122
  },
51270
52123
  async executeMove(move, ctx) {
51271
- const currentTask = await ctx.client.get("Task", move.cardId);
52124
+ const currentTask = await ctx.client.get("Task", move.cardId, {
52125
+ skipCache: true
52126
+ });
51272
52127
  const serverColumn = columnIdForTaskStatus(currentTask.status);
51273
52128
  if (serverColumn !== move.fromColumn) {
51274
52129
  return {
@@ -51299,8 +52154,19 @@ var taskKanbanAdapter = {
51299
52154
  errorMessage: "Unknown Task column"
51300
52155
  };
51301
52156
  }
52157
+ const modified = currentTask.modified;
52158
+ if (typeof modified !== "string" || !modified.trim()) {
52159
+ return {
52160
+ ok: false,
52161
+ cardId: move.cardId,
52162
+ fromColumn: serverColumn,
52163
+ toColumn: move.toColumn,
52164
+ errorMessage: "Cannot move Task without a modified timestamp. Refresh the board and try again."
52165
+ };
52166
+ }
51302
52167
  const serverTask = await ctx.client.update("Task", move.cardId, {
51303
- status
52168
+ status,
52169
+ modified
51304
52170
  });
51305
52171
  return {
51306
52172
  ok: true,
@@ -51531,7 +52397,8 @@ var opportunityKanbanAdapter = {
51531
52397
  async executeMove(move, ctx) {
51532
52398
  const currentOpportunity = await ctx.client.get(
51533
52399
  "Opportunity",
51534
- move.cardId
52400
+ move.cardId,
52401
+ { skipCache: true }
51535
52402
  );
51536
52403
  const serverColumn = columnIdForOpportunityStatus(
51537
52404
  currentOpportunity.status
@@ -51565,11 +52432,22 @@ var opportunityKanbanAdapter = {
51565
52432
  errorMessage: "Unknown Opportunity column"
51566
52433
  };
51567
52434
  }
52435
+ const modified = currentOpportunity.modified;
52436
+ if (typeof modified !== "string" || !modified.trim()) {
52437
+ return {
52438
+ ok: false,
52439
+ cardId: move.cardId,
52440
+ fromColumn: serverColumn,
52441
+ toColumn: move.toColumn,
52442
+ errorMessage: "Cannot move Opportunity without a modified timestamp. Refresh the board and try again."
52443
+ };
52444
+ }
51568
52445
  const serverOpportunity = await ctx.client.update(
51569
52446
  "Opportunity",
51570
52447
  move.cardId,
51571
52448
  {
51572
- status
52449
+ status,
52450
+ modified
51573
52451
  }
51574
52452
  );
51575
52453
  return {
@@ -51742,7 +52620,9 @@ var issueKanbanAdapter = {
51742
52620
  return { allowed: true };
51743
52621
  },
51744
52622
  async executeMove(move, ctx) {
51745
- const currentIssue = await ctx.client.get("Issue", move.cardId);
52623
+ const currentIssue = await ctx.client.get("Issue", move.cardId, {
52624
+ skipCache: true
52625
+ });
51746
52626
  const serverColumn = columnIdForIssueStatus(currentIssue.status);
51747
52627
  if (serverColumn !== move.fromColumn) {
51748
52628
  return {
@@ -51773,8 +52653,19 @@ var issueKanbanAdapter = {
51773
52653
  errorMessage: "Unknown Issue column"
51774
52654
  };
51775
52655
  }
52656
+ const modified = currentIssue.modified;
52657
+ if (typeof modified !== "string" || !modified.trim()) {
52658
+ return {
52659
+ ok: false,
52660
+ cardId: move.cardId,
52661
+ fromColumn: serverColumn,
52662
+ toColumn: move.toColumn,
52663
+ errorMessage: "Cannot move Issue without a modified timestamp. Refresh the board and try again."
52664
+ };
52665
+ }
51776
52666
  const serverIssue = await ctx.client.update("Issue", move.cardId, {
51777
- status
52667
+ status,
52668
+ modified
51778
52669
  });
51779
52670
  return {
51780
52671
  ok: true,
@@ -51792,6 +52683,10 @@ var ADAPTERS = {
51792
52683
  opportunity: opportunityKanbanAdapter,
51793
52684
  issue: issueKanbanAdapter
51794
52685
  };
52686
+ var FILTER_STATUSES = {
52687
+ Issue: ["Open", "Replied", "On Hold", "Resolved", "Closed"],
52688
+ Opportunity: ["Open", "Replied", "Quotation", "Converted", "Closed", "Lost"]
52689
+ };
51795
52690
  function getAdapter(doctype) {
51796
52691
  const definition = getKanbanBoardDefinition(doctype);
51797
52692
  if (!definition) {
@@ -51859,8 +52754,8 @@ var kanbanTools = [
51859
52754
  },
51860
52755
  status: {
51861
52756
  type: "string",
51862
- description: "Optional Opportunity status filter",
51863
- enum: ["Open", "Replied", "Quotation", "Converted", "Closed", "Lost"]
52757
+ description: "Optional Issue or Opportunity status filter; not supported for Task. Status must belong to the selected DocType.",
52758
+ enum: [...new Set(Object.values(FILTER_STATUSES).flat())]
51864
52759
  },
51865
52760
  opportunity_owner: {
51866
52761
  type: "string",
@@ -51884,6 +52779,12 @@ var kanbanTools = [
51884
52779
  handler: async (input, ctx) => {
51885
52780
  const doctype = String(input.doctype ?? "");
51886
52781
  const { definition, adapter } = getAdapter(doctype);
52782
+ const statuses = FILTER_STATUSES[doctype];
52783
+ if (statuses && input.status !== void 0 && (typeof input.status !== "string" || !statuses.includes(input.status))) {
52784
+ throw new Error(
52785
+ `[erpnext_kanban_get_board] Invalid status for ${doctype}. Expected one of: ${statuses.join(", ")}`
52786
+ );
52787
+ }
51887
52788
  const limit = normalizeLimit(Number(input.limit ?? 50));
51888
52789
  const offset = Math.max(0, Number(input.offset ?? 0));
51889
52790
  const rows = await ctx.client.list(doctype, {
@@ -52915,12 +53816,17 @@ function unquote(value) {
52915
53816
  }
52916
53817
  function optionalEnvValue(value) {
52917
53818
  const trimmed = value?.trim();
52918
- return trimmed ? unquote(trimmed) : void 0;
53819
+ if (!trimmed) return void 0;
53820
+ const unquoted = unquote(trimmed);
53821
+ return unquoted.trim() ? unquoted : void 0;
52919
53822
  }
52920
53823
  function loadAuthConfig2() {
52921
53824
  const single = optionalEnvValue(env6("MCP_AUTH_TOKEN"));
52922
53825
  const multi = env6("MCP_AUTH_TOKENS");
52923
53826
  const jwksUrl = optionalEnvValue(env6("MCP_OAUTH_JWKS_URL"));
53827
+ const audience = optionalEnvValue(env6("MCP_OAUTH_AUDIENCE"));
53828
+ const issuer = optionalEnvValue(env6("MCP_OAUTH_ISSUER"));
53829
+ const resource = optionalEnvValue(env6("MCP_AUTH_RESOURCE"));
52924
53830
  const tokens = /* @__PURE__ */ new Set();
52925
53831
  if (single) tokens.add(single);
52926
53832
  if (multi) {
@@ -52929,13 +53835,36 @@ function loadAuthConfig2() {
52929
53835
  if (token) tokens.add(token);
52930
53836
  }
52931
53837
  }
52932
- if (tokens.size === 0 && !jwksUrl) return null;
53838
+ const oauthConfigured = Boolean(jwksUrl || audience || issuer);
53839
+ if (oauthConfigured) {
53840
+ const requiredOAuthValues = [
53841
+ ["MCP_OAUTH_JWKS_URL", jwksUrl],
53842
+ ["MCP_OAUTH_AUDIENCE", audience],
53843
+ ["MCP_OAUTH_ISSUER", issuer],
53844
+ ["MCP_AUTH_RESOURCE", resource]
53845
+ ];
53846
+ for (const [name, value] of requiredOAuthValues) {
53847
+ if (!value) {
53848
+ throw new Error(
53849
+ `[hvgerp-mcp] ${name} is required when OAuth configuration is present`
53850
+ );
53851
+ }
53852
+ }
53853
+ }
53854
+ if (tokens.size === 0 && !oauthConfigured) {
53855
+ if (resource) {
53856
+ throw new Error(
53857
+ "[hvgerp-mcp] MCP_AUTH_RESOURCE requires a static token or OAuth configuration"
53858
+ );
53859
+ }
53860
+ return null;
53861
+ }
52933
53862
  return {
52934
53863
  tokens,
52935
- resource: optionalEnvValue(env6("MCP_AUTH_RESOURCE")),
53864
+ resource,
52936
53865
  jwksUrl,
52937
- audience: optionalEnvValue(env6("MCP_OAUTH_AUDIENCE")),
52938
- issuer: optionalEnvValue(env6("MCP_OAUTH_ISSUER"))
53866
+ audience,
53867
+ issuer
52939
53868
  };
52940
53869
  }
52941
53870
  function buildAuthProvider(config2) {
@@ -53172,7 +54101,7 @@ function installClaudeCodeStdioCompat(cacheHints) {
53172
54101
  }
53173
54102
 
53174
54103
  // src/version.ts
53175
- var SERVER_VERSION = "3.4.0";
54104
+ var SERVER_VERSION = "3.5.0";
53176
54105
 
53177
54106
  // src/instructions.ts
53178
54107
  var HEADER_OPENING = `This server exposes a live ERPNext/Frappe instance (HVG Group).
@@ -53217,13 +54146,37 @@ var PERMISSIONS = {
53217
54146
  have permission" or "you have none" - say the deployment's account could not read it. A refusal
53218
54147
  is still an answer: do not retry the same read through a different tool hoping for a wider view.`
53219
54148
  };
53220
- var WRITES = `WRITES
53221
- - Some tools change live business data, and the NAME does not say which: \`erpnext_doc_assign\`,
53222
- \`erpnext_doc_unassign\`, \`erpnext_kanban_move_card\`, \`erpnext_file_upload\` and
53223
- \`erpnext_method_call\` all write without a _create/_update/_submit/_cancel/_delete suffix, and
53224
- the last of those reaches any allowlisted business method. Every writing tool carries
53225
- \`readOnlyHint: false\` in its \`tools/list\` annotations - read that rather than the name, and
53226
- confirm the details with the user before calling one.`;
54149
+ var SILENT_WRITES = [
54150
+ { name: "erpnext_doc_assign" },
54151
+ { name: "erpnext_doc_unassign" },
54152
+ { name: "erpnext_kanban_move_card" },
54153
+ { name: "erpnext_file_upload" },
54154
+ {
54155
+ name: "erpnext_attendance_day_fix",
54156
+ note: "`erpnext_attendance_day_fix` goes further still: repairing a day CANCELS the\n Attendance record already standing on it."
54157
+ },
54158
+ {
54159
+ name: "erpnext_method_call",
54160
+ note: "`erpnext_method_call` reaches any allowlisted business method."
54161
+ }
54162
+ ];
54163
+ var WRITES_RULE = "- Every writing tool carries `readOnlyHint: false` in its `tools/list` annotations - read\n that rather than the name, and confirm the details with the user before calling one.";
54164
+ function buildWrites(loaded) {
54165
+ const present = SILENT_WRITES.filter((tool) => loaded.has(tool.name));
54166
+ const lines = ["WRITES"];
54167
+ if (present.length > 0) {
54168
+ const names = present.map((tool) => `\`${tool.name}\``).join(", ");
54169
+ lines.push(
54170
+ `- Some tools change live business data, and the NAME does not say which: ${names}
54171
+ all write without a _create/_update/_submit/_cancel/_delete suffix.`
54172
+ );
54173
+ for (const tool of present) {
54174
+ if (tool.note) lines.push(`- ${tool.note}`);
54175
+ }
54176
+ }
54177
+ lines.push(WRITES_RULE);
54178
+ return lines.join("\n");
54179
+ }
53227
54180
  function buildServerInstructions(mode, loadedToolNames) {
53228
54181
  const loaded = new Set(loadedToolNames);
53229
54182
  const header = [
@@ -53232,7 +54185,7 @@ function buildServerInstructions(mode, loadedToolNames) {
53232
54185
  (line) => line.requires === void 0 || loaded.has(line.requires)
53233
54186
  ).map((line) => line.text)
53234
54187
  ].join("\n");
53235
- return [header, PERMISSIONS[mode], WRITES].join("\n\n");
54188
+ return [header, PERMISSIONS[mode], buildWrites(loaded)].join("\n\n");
53236
54189
  }
53237
54190
 
53238
54191
  // server.ts