@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/README.md +16 -8
- package/hvgerp-mcp.mjs +1942 -989
- package/package.json +1 -1
- package/ui-dist/chart-viewer/index.html +35 -35
- package/ui-dist/doclist-viewer/index.html +21 -22
- package/ui-dist/funnel-viewer/index.html +36 -36
- package/ui-dist/invoice-viewer/index.html +35 -35
- package/ui-dist/kanban-viewer/index.html +48 -48
- package/ui-dist/kpi-viewer/index.html +38 -38
- package/ui-dist/stock-viewer/index.html +39 -39
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
22955
|
-
if (
|
|
22956
|
-
return
|
|
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(
|
|
32961
|
+
this.#addRoute(methodName, this.#path, args1);
|
|
32950
32962
|
}
|
|
32951
32963
|
args.forEach((handler) => {
|
|
32952
|
-
this.#addRoute(
|
|
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
|
-
|
|
32962
|
-
|
|
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
|
-
|
|
35439
|
-
|
|
35440
|
-
|
|
35441
|
-
|
|
35442
|
-
|
|
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
|
-
|
|
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(
|
|
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/
|
|
35554
|
+
// node_modules/jose/dist/webapi/lib/validate.js
|
|
35632
35555
|
function isObject2(input) {
|
|
35633
|
-
if (typeof input
|
|
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
|
-
|
|
35644
|
-
|
|
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
|
-
|
|
35673
|
-
|
|
35674
|
-
|
|
35675
|
-
|
|
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
|
|
35695
|
-
|
|
35696
|
-
|
|
35697
|
-
|
|
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
|
-
|
|
35700
|
-
|
|
35701
|
-
|
|
35702
|
-
|
|
35703
|
-
|
|
35704
|
-
|
|
35705
|
-
|
|
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
|
|
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
|
|
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
|
|
35654
|
+
return key;
|
|
35655
|
+
let normalized, keyObject;
|
|
35735
35656
|
if (isObject2(key)) {
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
35742
|
-
|
|
35743
|
-
|
|
35744
|
-
|
|
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
|
|
35756
|
-
throw
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35760
|
-
|
|
35761
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35778
|
-
|
|
35779
|
-
|
|
35780
|
-
|
|
35781
|
-
|
|
35782
|
-
|
|
35783
|
-
|
|
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
|
-
|
|
35802
|
-
|
|
35803
|
-
|
|
35804
|
-
|
|
35805
|
-
|
|
35806
|
-
|
|
35807
|
-
|
|
35808
|
-
|
|
35809
|
-
|
|
35810
|
-
|
|
35811
|
-
|
|
35812
|
-
|
|
35813
|
-
|
|
35814
|
-
|
|
35815
|
-
|
|
35816
|
-
|
|
35817
|
-
|
|
35818
|
-
|
|
35819
|
-
|
|
35820
|
-
|
|
35821
|
-
|
|
35822
|
-
|
|
35823
|
-
|
|
35824
|
-
|
|
35825
|
-
|
|
35826
|
-
|
|
35827
|
-
|
|
35828
|
-
|
|
35829
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
35988
|
-
return
|
|
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
|
|
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
|
-
|
|
36011
|
-
|
|
36012
|
-
|
|
36013
|
-
|
|
36014
|
-
|
|
36015
|
-
|
|
36016
|
-
|
|
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
|
-
|
|
36023
|
-
|
|
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
|
-
|
|
36060
|
-
invalidDuration();
|
|
36061
|
-
}
|
|
35911
|
+
typeof str != "string" && invalidDuration();
|
|
36062
35912
|
const matched = REGEX.exec(str);
|
|
36063
|
-
|
|
36064
|
-
|
|
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
|
-
|
|
36099
|
-
|
|
36100
|
-
|
|
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
|
|
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
|
-
|
|
36121
|
-
const
|
|
36122
|
-
|
|
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
|
|
35957
|
+
if (typeof clockTolerance == "string")
|
|
36147
35958
|
tolerance = secs(clockTolerance);
|
|
36148
|
-
|
|
36149
|
-
if (typeof clockTolerance
|
|
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
|
-
|
|
36157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 } =
|
|
36200
|
-
|
|
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
|
|
36205
|
-
|
|
36206
|
-
|
|
36207
|
-
|
|
36208
|
-
|
|
36209
|
-
|
|
36210
|
-
}
|
|
36211
|
-
|
|
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
|
|
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
|
-
|
|
36225
|
-
|
|
36226
|
-
|
|
36227
|
-
|
|
36228
|
-
|
|
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
|
-
|
|
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(
|
|
36033
|
+
yield await importWithAlgCache(cached2, jwk2, entry);
|
|
36242
36034
|
} catch {
|
|
36243
36035
|
}
|
|
36244
|
-
|
|
36245
|
-
};
|
|
36246
|
-
throw error2;
|
|
36036
|
+
}, error2;
|
|
36247
36037
|
}
|
|
36248
|
-
return importWithAlgCache(
|
|
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
|
|
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
|
-
|
|
36261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36308
|
-
const
|
|
36309
|
-
|
|
36310
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
36352
|
-
pendingFetch = void 0;
|
|
36353
|
-
}
|
|
36101
|
+
pendingFetch === current && (pendingFetch = void 0);
|
|
36354
36102
|
});
|
|
36355
36103
|
}
|
|
36356
36104
|
await pendingFetch;
|
|
36357
36105
|
};
|
|
36358
|
-
|
|
36359
|
-
|
|
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
|
|
36476
|
-
if (
|
|
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:
|
|
36222
|
+
subject: cached2.authInfo.subject ?? ""
|
|
36480
36223
|
});
|
|
36481
36224
|
}
|
|
36482
|
-
return
|
|
36225
|
+
return cached2.authInfo;
|
|
36483
36226
|
}
|
|
36484
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
42019
|
-
if (
|
|
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
|
|
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
|
|
42060
|
-
if (
|
|
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
|
|
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 [
|
|
42354
|
-
if (entry.expiresAt <= now) profileCache.delete(
|
|
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
|
|
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)
|
|
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
|
|
45212
|
-
const
|
|
45216
|
+
async function siteNow(ctx) {
|
|
45217
|
+
const { zone, authoritative } = await resolveTimeZone(ctx);
|
|
45213
45218
|
const now = /* @__PURE__ */ new Date();
|
|
45214
|
-
if (
|
|
45219
|
+
if (zone) {
|
|
45215
45220
|
try {
|
|
45216
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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';
|
|
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
|
|
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
|
-
|
|
46200
|
-
|
|
46201
|
-
|
|
46202
|
-
|
|
46203
|
-
|
|
46204
|
-
|
|
46205
|
-
|
|
46206
|
-
|
|
46207
|
-
|
|
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
|
|
49463
|
-
|
|
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 (
|
|
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)
|
|
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
|
-
"
|
|
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
|
-
`
|
|
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
|
|
49529
|
-
|
|
49530
|
-
|
|
49531
|
-
|
|
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
|
|
49536
|
-
|
|
49537
|
-
|
|
49538
|
-
|
|
49539
|
-
|
|
49540
|
-
|
|
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
|
|
49547
|
-
byItem[item].qty +=
|
|
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,
|
|
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
|
-
|
|
49602
|
-
|
|
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
|
|
49606
|
-
fields: ["name", "status", "
|
|
49607
|
-
filters
|
|
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) + (
|
|
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
|
|
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
|
|
49630
|
-
fields: ["item_code", "item_name", "
|
|
49631
|
-
filters
|
|
49632
|
-
|
|
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 +=
|
|
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
|
|
50467
|
+
currency,
|
|
49658
50468
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49659
50469
|
_meta: CHART_META
|
|
49660
50470
|
};
|
|
49661
50471
|
}
|
|
49662
|
-
const invoices = await
|
|
49663
|
-
fields: ["customer", "customer_name", "
|
|
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 +=
|
|
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
|
|
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: "
|
|
49708
|
-
|
|
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
|
|
49727
|
-
const
|
|
49728
|
-
|
|
49729
|
-
|
|
49730
|
-
|
|
49731
|
-
|
|
49732
|
-
|
|
49733
|
-
|
|
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
|
|
49754
|
-
|
|
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] +=
|
|
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
|
|
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
|
|
49786
|
-
|
|
50590
|
+
const mIdx = analyticsMonthIndex(
|
|
50591
|
+
order2.transaction_date,
|
|
50592
|
+
window2.monthKeys
|
|
50593
|
+
);
|
|
49787
50594
|
if (mIdx >= 0 && mIdx < monthsBack) {
|
|
49788
|
-
monthlyTotals[mIdx] +=
|
|
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
|
|
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,
|
|
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
|
|
49834
|
-
fields: ["customer_name", "status", "
|
|
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) + (
|
|
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
|
|
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) + (
|
|
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
|
|
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,
|
|
50725
|
+
handler: async (input, _ctx, context2) => {
|
|
50726
|
+
const { currency } = context2;
|
|
49919
50727
|
const limit = normalizeLimit(input.limit ?? 8);
|
|
49920
|
-
const orders = await
|
|
49921
|
-
fields: ["customer_name", "
|
|
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 +=
|
|
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:
|
|
50763
|
+
yAxisLabel: `Revenue (${currency})`,
|
|
49957
50764
|
rightAxisLabel: "# Orders",
|
|
49958
|
-
currency
|
|
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,
|
|
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
|
|
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) + (
|
|
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
|
|
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,
|
|
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
|
|
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 = [
|
|
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) =>
|
|
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 + (
|
|
50901
|
+
(s, b) => s + analyticsNumber(b, "actual_qty"),
|
|
50090
50902
|
0
|
|
50091
50903
|
);
|
|
50092
50904
|
const totalVal = bins.reduce(
|
|
50093
|
-
(s, b) => s + (
|
|
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
|
|
50099
|
-
fields: ["item_code", "qty", "
|
|
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] +=
|
|
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
|
|
50151
|
-
|
|
50152
|
-
|
|
50153
|
-
|
|
50154
|
-
|
|
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
|
|
50974
|
+
if (!Object.hasOwn(priceMap, code)) priceMap[code] = ip;
|
|
50160
50975
|
}
|
|
50161
|
-
const soItems = await
|
|
50162
|
-
fields: ["item_code", "
|
|
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) + (
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
50229
|
-
const
|
|
50230
|
-
const
|
|
50231
|
-
|
|
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", ">=",
|
|
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
|
|
50242
|
-
|
|
50243
|
-
|
|
51072
|
+
const idx = analyticsMonthIndex(
|
|
51073
|
+
o.transaction_date,
|
|
51074
|
+
window2.monthKeys
|
|
51075
|
+
);
|
|
50244
51076
|
if (idx >= 0 && idx < 6) {
|
|
50245
|
-
sparkline[idx] +=
|
|
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
|
|
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
|
|
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
|
|
50275
|
-
|
|
50276
|
-
|
|
50277
|
-
|
|
50278
|
-
|
|
50279
|
-
|
|
50280
|
-
|
|
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 +
|
|
51116
|
+
(sum, inv) => sum + inv.outstanding_amount,
|
|
50284
51117
|
0
|
|
50285
51118
|
);
|
|
50286
|
-
const count = invoices.
|
|
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
|
|
50291
|
-
currency
|
|
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
|
|
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
|
|
50309
|
-
const
|
|
50310
|
-
|
|
50311
|
-
|
|
50312
|
-
|
|
50313
|
-
|
|
50314
|
-
|
|
50315
|
-
|
|
50316
|
-
|
|
50317
|
-
|
|
50318
|
-
|
|
50319
|
-
|
|
50320
|
-
|
|
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
|
|
50324
|
-
|
|
50325
|
-
|
|
50326
|
-
|
|
50327
|
-
["
|
|
50328
|
-
[
|
|
50329
|
-
|
|
50330
|
-
|
|
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,
|
|
50357
|
-
const soItems = await
|
|
50358
|
-
fields: ["item_code", "
|
|
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 + (
|
|
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) + (
|
|
51205
|
+
itemQty[code] = (itemQty[code] ?? 0) + analyticsNumber(row, "stock_qty");
|
|
50372
51206
|
}
|
|
50373
|
-
const bins = await
|
|
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] =
|
|
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
|
|
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
|
|
50413
|
-
const
|
|
50414
|
-
|
|
50415
|
-
|
|
50416
|
-
|
|
50417
|
-
|
|
50418
|
-
|
|
50419
|
-
|
|
50420
|
-
|
|
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 +
|
|
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
|
|
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
|
|
50458
|
-
|
|
50459
|
-
|
|
50460
|
-
|
|
50461
|
-
|
|
50462
|
-
|
|
50463
|
-
|
|
50464
|
-
|
|
50465
|
-
|
|
50466
|
-
|
|
50467
|
-
const
|
|
50468
|
-
|
|
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
|
-
|
|
50480
|
-
fields: ["name", "
|
|
50481
|
-
filters: txnFilters
|
|
50482
|
-
limit: 500
|
|
51315
|
+
context2.listAllDocuments("Opportunity", {
|
|
51316
|
+
fields: ["name", "base_opportunity_amount"],
|
|
51317
|
+
filters: txnFilters
|
|
50483
51318
|
}),
|
|
50484
|
-
|
|
50485
|
-
fields: ["name", "
|
|
50486
|
-
filters: submittedTxnFilters
|
|
50487
|
-
limit: 500
|
|
51319
|
+
context2.listAllDocuments("Quotation", {
|
|
51320
|
+
fields: ["name", "base_grand_total"],
|
|
51321
|
+
filters: submittedTxnFilters
|
|
50488
51322
|
}),
|
|
50489
|
-
|
|
50490
|
-
fields: ["name", "
|
|
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 + (
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
50567
|
-
|
|
50568
|
-
|
|
50569
|
-
|
|
50570
|
-
|
|
50571
|
-
|
|
50572
|
-
|
|
50573
|
-
|
|
50574
|
-
|
|
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] +=
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
50683
|
-
fields: [
|
|
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
|
-
|
|
50686
|
-
order_by: "amount desc"
|
|
51529
|
+
order_by: "base_amount desc"
|
|
50687
51530
|
}),
|
|
50688
51531
|
// Bin for valuation_rate (cost per unit)
|
|
50689
|
-
|
|
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 ?
|
|
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
|
-
|
|
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 =
|
|
50721
|
-
const unitCost = costMap[row.item_code]
|
|
50722
|
-
|
|
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
|
|
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 =
|
|
50770
|
-
const unitCost = costMap[code]
|
|
50771
|
-
|
|
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
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
53864
|
+
resource,
|
|
52936
53865
|
jwksUrl,
|
|
52937
|
-
audience
|
|
52938
|
-
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.
|
|
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
|
|
53221
|
-
|
|
53222
|
-
|
|
53223
|
-
|
|
53224
|
-
|
|
53225
|
-
|
|
53226
|
-
|
|
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],
|
|
54188
|
+
return [header, PERMISSIONS[mode], buildWrites(loaded)].join("\n\n");
|
|
53236
54189
|
}
|
|
53237
54190
|
|
|
53238
54191
|
// server.ts
|