@camstack/system 1.2.146 → 1.2.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/auth/auth-manager.d.ts +17 -1
- package/dist/auth/token-expiry.d.ts +39 -0
- package/dist/auth/user-manager.d.ts +2 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/auth-config-reader.d.ts +20 -0
- package/dist/builtins/local-auth/local-auth.addon.js +211 -17
- package/dist/builtins/local-auth/local-auth.addon.mjs +211 -17
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/load-series-config.d.ts +74 -0
- package/dist/builtins/native-metrics/load-series-reader.d.ts +37 -0
- package/dist/builtins/native-metrics/load-series-store.d.ts +232 -0
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +118 -35
- package/dist/builtins/native-metrics/native-metrics.addon.js +1463 -199
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1463 -199
- package/dist/builtins/native-metrics/node-load-ring.d.ts +214 -0
- package/dist/builtins/native-metrics/process-classification.d.ts +51 -0
- package/dist/builtins/native-metrics/thread-cpu-sampler.d.ts +116 -0
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +25 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +2 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -1
- package/dist/builtins/system-config/system-config.addon.js +12 -11
- package/dist/builtins/system-config/system-config.addon.mjs +12 -11
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-BVU5JADq.js → dist-B-mBrEz9.js} +497 -36
- package/dist/{dist-CG7kwIZp.mjs → dist-BwD83D2f.mjs} +480 -37
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{manifest-python-deps-CktMcXzS.js → manifest-python-deps-BV_Cy99l.js} +1 -1
- package/dist/{manifest-python-deps-CVeckyXx.mjs → manifest-python-deps-DpRX9LtD.mjs} +1 -1
- package/dist/{retired-settings-keys-Dm1z4mO4.mjs → retired-settings-keys-CRKpAmSn.mjs} +1 -1
- package/dist/{retired-settings-keys-_PLI9w0k.js → retired-settings-keys-Davtjo5p.js} +1 -1
- package/package.json +6 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import { E as authProviderCapability,
|
|
2
|
+
import { E as authProviderCapability, Tt as userManagementCapability, kt as BaseAddon, r as ApiKeyRecordSchema, v as ScopedTokenSchema, x as UserRecordSchema } from "../../dist-BwD83D2f.mjs";
|
|
3
3
|
import * as crypto$2 from "node:crypto";
|
|
4
4
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
5
5
|
import nodeCrypto from "crypto";
|
|
@@ -6000,7 +6000,7 @@ var require_sign = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6000
6000
|
};
|
|
6001
6001
|
}));
|
|
6002
6002
|
//#endregion
|
|
6003
|
-
//#region src/auth/
|
|
6003
|
+
//#region src/auth/token-expiry.ts
|
|
6004
6004
|
var import_jsonwebtoken = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
6005
6005
|
module.exports = {
|
|
6006
6006
|
decode: require_decode(),
|
|
@@ -6011,6 +6011,156 @@ var import_jsonwebtoken = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin
|
|
|
6011
6011
|
TokenExpiredError: require_TokenExpiredError()
|
|
6012
6012
|
};
|
|
6013
6013
|
})))());
|
|
6014
|
+
/**
|
|
6015
|
+
* Validation for the operator-configured session-JWT lifetime
|
|
6016
|
+
* (`auth.tokenExpiry`, System → Settings → Authentication → Token Expiry).
|
|
6017
|
+
*
|
|
6018
|
+
* WHY THIS EXISTS. The field is a free-text box that reaches `jwt.sign`'s
|
|
6019
|
+
* `expiresIn`. Two things go wrong without a gate in front of it:
|
|
6020
|
+
*
|
|
6021
|
+
* 1. `jsonwebtoken` THROWS on a value it cannot parse. The only caller is
|
|
6022
|
+
* the login/refresh path, so one typo in a settings form turns every
|
|
6023
|
+
* login into a 500 — a self-inflicted outage from a text field.
|
|
6024
|
+
* 2. `jsonwebtoken` hands a bare numeric STRING to `ms()`, which reads it
|
|
6025
|
+
* as MILLISECONDS. An operator who types `3600` meaning "an hour" gets
|
|
6026
|
+
* a token that dies in 3.6 seconds. A bare number is therefore rejected
|
|
6027
|
+
* rather than interpreted — the field's own description, placeholder and
|
|
6028
|
+
* default are all duration strings (`24h`, `7d`, `30d`).
|
|
6029
|
+
*
|
|
6030
|
+
* A rejection never becomes an unbounded lifetime — it resolves to
|
|
6031
|
+
* {@link DEFAULT_TOKEN_EXPIRY}, the single documented default — and it is
|
|
6032
|
+
* never silent: `parseTokenExpiry` reports the refusal in its result and
|
|
6033
|
+
* `logTokenExpiryRejection` renders it. The two are separate so the caller
|
|
6034
|
+
* owns how often it speaks; the signer re-reads on every mint.
|
|
6035
|
+
*/
|
|
6036
|
+
/**
|
|
6037
|
+
* The `ms`-compatible duration grammar `jsonwebtoken` accepts, MINUS the bare
|
|
6038
|
+
* number (see the docblock above). A unit is mandatory.
|
|
6039
|
+
*/
|
|
6040
|
+
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|milliseconds?|msecs?|s|secs?|seconds?|m|mins?|minutes?|h|hrs?|hours?|d|days?|w|weeks?|y|yrs?|years?)$/i;
|
|
6041
|
+
/** Multiplier from each accepted unit to milliseconds. */
|
|
6042
|
+
var UNIT_MS = {
|
|
6043
|
+
ms: 1,
|
|
6044
|
+
msec: 1,
|
|
6045
|
+
msecs: 1,
|
|
6046
|
+
millisecond: 1,
|
|
6047
|
+
milliseconds: 1,
|
|
6048
|
+
s: 1e3,
|
|
6049
|
+
sec: 1e3,
|
|
6050
|
+
secs: 1e3,
|
|
6051
|
+
second: 1e3,
|
|
6052
|
+
seconds: 1e3,
|
|
6053
|
+
m: 6e4,
|
|
6054
|
+
min: 6e4,
|
|
6055
|
+
mins: 6e4,
|
|
6056
|
+
minute: 6e4,
|
|
6057
|
+
minutes: 6e4,
|
|
6058
|
+
h: 36e5,
|
|
6059
|
+
hr: 36e5,
|
|
6060
|
+
hrs: 36e5,
|
|
6061
|
+
hour: 36e5,
|
|
6062
|
+
hours: 36e5,
|
|
6063
|
+
d: 864e5,
|
|
6064
|
+
day: 864e5,
|
|
6065
|
+
days: 864e5,
|
|
6066
|
+
w: 6048e5,
|
|
6067
|
+
week: 6048e5,
|
|
6068
|
+
weeks: 6048e5,
|
|
6069
|
+
y: 315576e5,
|
|
6070
|
+
yr: 315576e5,
|
|
6071
|
+
yrs: 315576e5,
|
|
6072
|
+
year: 315576e5,
|
|
6073
|
+
years: 315576e5
|
|
6074
|
+
};
|
|
6075
|
+
/**
|
|
6076
|
+
* Shortest accepted lifetime. Below a minute the token expires inside the
|
|
6077
|
+
* round trip that issued it and the operator sees "logged out immediately",
|
|
6078
|
+
* which reads as a broken server rather than as a bad setting.
|
|
6079
|
+
*/
|
|
6080
|
+
var MIN_EXPIRY_MS = 6e4;
|
|
6081
|
+
/**
|
|
6082
|
+
* Longest accepted lifetime. A year is already far past "a device left
|
|
6083
|
+
* unopened for weeks"; anything longer is an infinite-lifetime token wearing
|
|
6084
|
+
* a duration string.
|
|
6085
|
+
*/
|
|
6086
|
+
var MAX_EXPIRY_MS = 365 * 864e5;
|
|
6087
|
+
/** Milliseconds for a duration string, or `null` when it is not one. */
|
|
6088
|
+
function durationMs(candidate) {
|
|
6089
|
+
const match = DURATION_PATTERN.exec(candidate);
|
|
6090
|
+
if (match === null) return null;
|
|
6091
|
+
const amount = Number(match[1]);
|
|
6092
|
+
const unitMs = UNIT_MS[match[2].toLowerCase()];
|
|
6093
|
+
if (!Number.isFinite(amount) || unitMs === void 0) return null;
|
|
6094
|
+
return amount * unitMs;
|
|
6095
|
+
}
|
|
6096
|
+
/**
|
|
6097
|
+
* The default, pre-parsed. Computed from {@link DEFAULT_TOKEN_EXPIRY} rather
|
|
6098
|
+
* than written out again — a second spelling of the same default is the defect
|
|
6099
|
+
* this module exists to close.
|
|
6100
|
+
*/
|
|
6101
|
+
var DEFAULT_SECONDS = (() => {
|
|
6102
|
+
const ms = durationMs("30d");
|
|
6103
|
+
/* c8 ignore next -- unreachable unless DEFAULT_TOKEN_EXPIRY stops being a duration */
|
|
6104
|
+
if (ms === null) throw new Error(`DEFAULT_TOKEN_EXPIRY is not a duration: 30d`);
|
|
6105
|
+
return ms / 1e3;
|
|
6106
|
+
})();
|
|
6107
|
+
function refuse(reason) {
|
|
6108
|
+
return {
|
|
6109
|
+
ok: false,
|
|
6110
|
+
reason,
|
|
6111
|
+
seconds: DEFAULT_SECONDS,
|
|
6112
|
+
value: "30d"
|
|
6113
|
+
};
|
|
6114
|
+
}
|
|
6115
|
+
/**
|
|
6116
|
+
* Parse a configured `auth.tokenExpiry`. An absent/blank value is not a
|
|
6117
|
+
* rejection — it is "operator never set it" and resolves to the default
|
|
6118
|
+
* without a log line.
|
|
6119
|
+
*/
|
|
6120
|
+
function parseTokenExpiry(configured) {
|
|
6121
|
+
if (configured === void 0 || configured === null) return {
|
|
6122
|
+
ok: true,
|
|
6123
|
+
seconds: DEFAULT_SECONDS,
|
|
6124
|
+
value: "30d"
|
|
6125
|
+
};
|
|
6126
|
+
if (typeof configured !== "string") return refuse("not-a-string");
|
|
6127
|
+
const trimmed = configured.trim();
|
|
6128
|
+
if (trimmed === "") return {
|
|
6129
|
+
ok: true,
|
|
6130
|
+
seconds: DEFAULT_SECONDS,
|
|
6131
|
+
value: "30d"
|
|
6132
|
+
};
|
|
6133
|
+
const totalMs = durationMs(trimmed);
|
|
6134
|
+
if (totalMs === null) return refuse("malformed");
|
|
6135
|
+
if (totalMs < MIN_EXPIRY_MS) return refuse("too-short");
|
|
6136
|
+
if (totalMs > MAX_EXPIRY_MS) return refuse("too-long");
|
|
6137
|
+
return {
|
|
6138
|
+
ok: true,
|
|
6139
|
+
seconds: totalMs / 1e3,
|
|
6140
|
+
value: trimmed
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
/** Human-readable tail for the rejection log line. */
|
|
6144
|
+
var REJECTION_DETAIL = {
|
|
6145
|
+
"not-a-string": "value is not a string",
|
|
6146
|
+
malformed: "not a duration string (expected e.g. 24h, 7d, 30d — a bare number is NOT seconds)",
|
|
6147
|
+
"too-short": `shorter than the ${MIN_EXPIRY_MS / 1e3}s minimum`,
|
|
6148
|
+
"too-long": `longer than the ${MAX_EXPIRY_MS / 864e5}d maximum`
|
|
6149
|
+
};
|
|
6150
|
+
/**
|
|
6151
|
+
* Report a refused `auth.tokenExpiry`. Separate from {@link parseTokenExpiry}
|
|
6152
|
+
* so the caller owns HOW OFTEN it speaks — the signer re-reads the setting on
|
|
6153
|
+
* every mint and must not turn one bad save into a line per login.
|
|
6154
|
+
*/
|
|
6155
|
+
function logTokenExpiryRejection(configured, rejection, logger) {
|
|
6156
|
+
logger.warn(`auth.tokenExpiry REJECTED — ${REJECTION_DETAIL[rejection.reason]}; signing with the default 30d instead`, { meta: {
|
|
6157
|
+
configured,
|
|
6158
|
+
reason: rejection.reason,
|
|
6159
|
+
applied: rejection.value
|
|
6160
|
+
} });
|
|
6161
|
+
}
|
|
6162
|
+
//#endregion
|
|
6163
|
+
//#region src/auth/auth-manager.ts
|
|
6014
6164
|
/** Minimal no-op logger for default parameter */
|
|
6015
6165
|
var noopLogger = {
|
|
6016
6166
|
debug() {},
|
|
@@ -6028,6 +6178,16 @@ var AuthManager = class {
|
|
|
6028
6178
|
config;
|
|
6029
6179
|
jwtSecret;
|
|
6030
6180
|
logger;
|
|
6181
|
+
/**
|
|
6182
|
+
* Offending `auth.tokenExpiry` values already reported by this instance.
|
|
6183
|
+
*
|
|
6184
|
+
* `signToken` re-reads the setting on EVERY mint so an operator's change
|
|
6185
|
+
* takes effect without a restart — which also means a bad value would log
|
|
6186
|
+
* once per login and once per silent refresh from every viewer. The refusal
|
|
6187
|
+
* must be visible, not repeated: the first mint after a bad save logs, the
|
|
6188
|
+
* rest resolve silently to the same default until the value changes.
|
|
6189
|
+
*/
|
|
6190
|
+
reportedInvalidExpiries = /* @__PURE__ */ new Set();
|
|
6031
6191
|
constructor(config, logger = noopLogger) {
|
|
6032
6192
|
this.config = config;
|
|
6033
6193
|
this.logger = logger;
|
|
@@ -6042,8 +6202,15 @@ var AuthManager = class {
|
|
|
6042
6202
|
}
|
|
6043
6203
|
signToken(payload) {
|
|
6044
6204
|
const configured = this.config.get("auth.tokenExpiry");
|
|
6045
|
-
const
|
|
6046
|
-
|
|
6205
|
+
const parsed = parseTokenExpiry(configured);
|
|
6206
|
+
if (!parsed.ok) {
|
|
6207
|
+
const seen = typeof configured === "string" ? configured : JSON.stringify(configured);
|
|
6208
|
+
if (!this.reportedInvalidExpiries.has(seen)) {
|
|
6209
|
+
this.reportedInvalidExpiries.add(seen);
|
|
6210
|
+
logTokenExpiryRejection(configured, parsed, this.logger);
|
|
6211
|
+
}
|
|
6212
|
+
}
|
|
6213
|
+
return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: parsed.seconds });
|
|
6047
6214
|
}
|
|
6048
6215
|
verifyToken(token) {
|
|
6049
6216
|
return import_jsonwebtoken.verify(token, this.jwtSecret);
|
|
@@ -8814,6 +8981,38 @@ var TotpManager = class {
|
|
|
8814
8981
|
}
|
|
8815
8982
|
};
|
|
8816
8983
|
//#endregion
|
|
8984
|
+
//#region src/builtins/local-auth/auth-config-reader.ts
|
|
8985
|
+
/**
|
|
8986
|
+
* Every config path this reader can answer. Anything else resolves to
|
|
8987
|
+
* `undefined` by construction — see the module docblock.
|
|
8988
|
+
*/
|
|
8989
|
+
function createAuthConfigReader(resolved, writeAuthSection) {
|
|
8990
|
+
return {
|
|
8991
|
+
get(path) {
|
|
8992
|
+
switch (path) {
|
|
8993
|
+
case "auth.jwtSecret": return narrow(resolved.jwtSecret);
|
|
8994
|
+
case "auth.adminUsername": return narrow(resolved.adminUsername);
|
|
8995
|
+
case "auth.adminPassword": return narrow(resolved.adminPassword);
|
|
8996
|
+
case "auth.tokenExpiry": return narrow(resolved.tokenExpiry);
|
|
8997
|
+
default: return;
|
|
8998
|
+
}
|
|
8999
|
+
},
|
|
9000
|
+
update(_section, data) {
|
|
9001
|
+
if (typeof data["jwtSecret"] === "string") writeAuthSection({ jwtSecret: data["jwtSecret"] });
|
|
9002
|
+
}
|
|
9003
|
+
};
|
|
9004
|
+
}
|
|
9005
|
+
/**
|
|
9006
|
+
* The one documented type boundary in this module. `AuthConfigReader.get` is
|
|
9007
|
+
* the legacy `get<T>(path)` shape shared with `ConfigManager.get`, which is
|
|
9008
|
+
* likewise unchecked at runtime — every value returned here is a `string |
|
|
9009
|
+
* undefined` the caller immediately re-validates (`parseTokenExpiry`, the
|
|
9010
|
+
* truthiness gate on `jwtSecret`).
|
|
9011
|
+
*/
|
|
9012
|
+
function narrow(value) {
|
|
9013
|
+
return value;
|
|
9014
|
+
}
|
|
9015
|
+
//#endregion
|
|
8817
9016
|
//#region src/builtins/local-auth/auth-schema.ts
|
|
8818
9017
|
/**
|
|
8819
9018
|
* @durable class=config owner=local-auth
|
|
@@ -9462,19 +9661,14 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
9462
9661
|
}
|
|
9463
9662
|
async onInitialize() {
|
|
9464
9663
|
const authSection = await this.ctx.settings?.getSection("auth") ?? {};
|
|
9465
|
-
const
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
},
|
|
9474
|
-
update: (_section, data) => {
|
|
9475
|
-
if (typeof data["jwtSecret"] === "string") this.ctx.settings?.setSection("auth", { jwtSecret: data["jwtSecret"] });
|
|
9476
|
-
}
|
|
9477
|
-
};
|
|
9664
|
+
const reader = createAuthConfigReader({
|
|
9665
|
+
jwtSecret: typeof authSection["jwtSecret"] === "string" ? authSection["jwtSecret"] : this.config.jwtSecret ?? "",
|
|
9666
|
+
adminUsername: typeof authSection["adminUsername"] === "string" && authSection["adminUsername"] ? authSection["adminUsername"] : this.config.adminUsername ?? "",
|
|
9667
|
+
adminPassword: typeof authSection["adminPassword"] === "string" && authSection["adminPassword"] ? authSection["adminPassword"] : this.config.adminPassword ?? "",
|
|
9668
|
+
tokenExpiry: typeof authSection["tokenExpiry"] === "string" && authSection["tokenExpiry"] ? authSection["tokenExpiry"] : void 0
|
|
9669
|
+
}, (patch) => {
|
|
9670
|
+
this.ctx.settings?.setSection("auth", patch);
|
|
9671
|
+
});
|
|
9478
9672
|
this.authManager = new AuthManager(reader, this.ctx.logger);
|
|
9479
9673
|
const store = this.ctx.api?.settingsStore;
|
|
9480
9674
|
if (store) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-B-mBrEz9.js");
|
|
4
4
|
const require_tls = require("../../tls-u8QCJCFE.js");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as BaseAddon, nt as localNetworkCapability, tn as EventCategory } from "../../dist-BwD83D2f.mjs";
|
|
2
2
|
import { S as readLanHttpState, _ as DEFAULT_LAN_HTTP_PORT, a as writeTlsMode, i as writeExtraSans, l as reissueTlsLeaf, n as readTlsAccessStatus, o as validateUploadedTls, r as readTlsMode, s as ensureTlsCert, t as readExtraSans, y as applyLanHttp } from "../../tls-CQhPGSJm.mjs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-B-mBrEz9.js");
|
|
7
7
|
//#region src/builtins/loki-logging/loki-payload.ts
|
|
8
8
|
/**
|
|
9
9
|
* Loki rejects a label name that is not a valid Prometheus label
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kt as BaseAddon, rt as logDestinationCapability } from "../../dist-BwD83D2f.mjs";
|
|
2
2
|
//#region src/builtins/loki-logging/loki-payload.ts
|
|
3
3
|
/**
|
|
4
4
|
* Loki rejects a label name that is not a valid Prometheus label
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two knobs of the durable load series, and the arithmetic that makes them
|
|
3
|
+
* honest.
|
|
4
|
+
*
|
|
5
|
+
* ## Where they live, and why there
|
|
6
|
+
*
|
|
7
|
+
* In `native-metrics`'s OWN global settings document — the one that already
|
|
8
|
+
* exists and already carries `samplingIntervalMs`. Fields on a document, never
|
|
9
|
+
* a method per knob: `system.getLoggingSettings` / `setLoggingSettings` set
|
|
10
|
+
* that precedent and it holds here.
|
|
11
|
+
*
|
|
12
|
+
* They are NOT in the logging document, and that is a decision rather than an
|
|
13
|
+
* omission. That document is about levels and diagnostic windows; a metrics
|
|
14
|
+
* cadence and a table's retention are neither. Two documents that both claim a
|
|
15
|
+
* knob is how this repo has already shipped a switch nobody read.
|
|
16
|
+
*
|
|
17
|
+
* They are cluster-wide, not per-node. Every node emits on the same cadence or
|
|
18
|
+
* the fleet's series cannot be laid over each other, and the table is single
|
|
19
|
+
* and hub-resident, so a per-node retention would be a promise nothing could
|
|
20
|
+
* keep.
|
|
21
|
+
*
|
|
22
|
+
* ## Refused, not clamped
|
|
23
|
+
*
|
|
24
|
+
* A cadence outside 5-60 s is REFUSED. Clamping would silently store 10 when
|
|
25
|
+
* the operator typed 2 and then report 10 back as though it had been accepted
|
|
26
|
+
* — the failure mode where a knob and its readback agree on a value nobody
|
|
27
|
+
* chose. The schema advertises the bounds; this refuses anything else.
|
|
28
|
+
*/
|
|
29
|
+
/** The resolved, in-force configuration. */
|
|
30
|
+
export interface LoadSeriesConfig {
|
|
31
|
+
readonly cadenceSec: number;
|
|
32
|
+
readonly retentionHours: number;
|
|
33
|
+
readonly maxRows: number;
|
|
34
|
+
}
|
|
35
|
+
/** Why a value was refused — the message an operator is shown verbatim. */
|
|
36
|
+
export declare class LoadSeriesConfigError extends Error {
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the configuration, REFUSING anything out of range.
|
|
41
|
+
*
|
|
42
|
+
* An ABSENT field takes its default — that is not a refusal, it is a document
|
|
43
|
+
* that has never been written. A PRESENT field outside its bounds throws.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveLoadSeriesConfig(raw: {
|
|
46
|
+
readonly loadSeriesCadenceSec?: unknown;
|
|
47
|
+
readonly loadSeriesRetentionHours?: unknown;
|
|
48
|
+
}): LoadSeriesConfig;
|
|
49
|
+
/** What a configuration is projected to cost, from OBSERVED counts. */
|
|
50
|
+
export interface LoadSeriesProjection {
|
|
51
|
+
/** Rows the retention window alone would hold. */
|
|
52
|
+
readonly intendedRows: number;
|
|
53
|
+
/** Rows actually allowed — `min(intendedRows, maxRows)`. */
|
|
54
|
+
readonly boundedRows: number;
|
|
55
|
+
readonly estimatedBytes: number;
|
|
56
|
+
/** Does the row cap bite before the retention window is full? */
|
|
57
|
+
readonly capBites: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Project the cost of a configuration from the numbers the cluster is actually
|
|
61
|
+
* reporting — nodes and processes OBSERVED, never a constant.
|
|
62
|
+
*
|
|
63
|
+
* This is what makes the knob honest: an operator raising retention to three
|
|
64
|
+
* days sees the row count and the megabytes BEFORE applying it, rather than
|
|
65
|
+
* discovering them afterwards. `MEASURED_BYTES_PER_ROW` is measured, not
|
|
66
|
+
* guessed — see `load-series-store.ts`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function projectLoadSeriesCost(input: {
|
|
69
|
+
readonly config: LoadSeriesConfig;
|
|
70
|
+
/** Process rows observed across the fleet in the most recent snapshots. */
|
|
71
|
+
readonly observedProcessRows: number;
|
|
72
|
+
}): LoadSeriesProjection;
|
|
73
|
+
/** Human-readable one-liner for the settings form. */
|
|
74
|
+
export declare function describeLoadSeriesCost(projection: LoadSeriesProjection): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LoadPoint } from '@camstack/types';
|
|
2
|
+
import { RetainedSample } from './load-series-store.js';
|
|
3
|
+
/** One function's assembled series, before it is shaped for the wire. */
|
|
4
|
+
export interface FoldedSeries {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly kind: 'addon' | 'root' | 'unattributed';
|
|
7
|
+
readonly points: readonly LoadPoint[];
|
|
8
|
+
}
|
|
9
|
+
export interface MergeLoadSeriesInput {
|
|
10
|
+
/** Cold samples, oldest-first. May overlap `hot` — that is expected. */
|
|
11
|
+
readonly cold: readonly RetainedSample[];
|
|
12
|
+
/** Hot samples, oldest-first. Wins over a cold sample at the same `atMs`. */
|
|
13
|
+
readonly hot: readonly RetainedSample[];
|
|
14
|
+
readonly cadenceMs: number;
|
|
15
|
+
/** Absent ⇒ no reduction. */
|
|
16
|
+
readonly maxPoints?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface MergedLoadSeries {
|
|
19
|
+
readonly series: readonly FoldedSeries[];
|
|
20
|
+
readonly bucketMs: number;
|
|
21
|
+
readonly retainedSamples: number;
|
|
22
|
+
readonly oldestAtMs: number | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Merge the two tiers into one deduped, oldest-first list of samples.
|
|
26
|
+
*
|
|
27
|
+
* Exported because the dedup is the property worth testing on its own: a
|
|
28
|
+
* doubled point is invisible in a total and obvious only as a chart that reads
|
|
29
|
+
* twice as loud as the machine.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mergeSamples(cold: readonly RetainedSample[], hot: readonly RetainedSample[]): readonly RetainedSample[];
|
|
32
|
+
/**
|
|
33
|
+
* Merge, fold and reduce. The whole read path in one pure function, so every
|
|
34
|
+
* property that matters — dedup, peak survival, an empty bucket staying empty
|
|
35
|
+
* — is testable without a database or a ring.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mergeLoadSeries(input: MergeLoadSeriesInput): MergedLoadSeries;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { IScopedLogger, SettingsStoreClient } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* @durable class=audit owner=metrics-native
|
|
4
|
+
* write="one row per process per sample, appended in ONE transaction per
|
|
5
|
+
* sample (`settingsStore.insertMany`) by the HUB's `native-metrics`, off its
|
|
6
|
+
* subscription to `metrics.node-processes-snapshot`. Emission is
|
|
7
|
+
* fixed-cadence, so the write rate is deterministic: nodes x processes /
|
|
8
|
+
* cadence, 7.6 rows/s on the live fleet at the 10 s default. Only a sample
|
|
9
|
+
* the in-memory ring ACCEPTED is written, which is what makes the append
|
|
10
|
+
* idempotent without a unique key."
|
|
11
|
+
* retention="TWO bounds, swept together at most once a minute. By AGE, the
|
|
12
|
+
* operator's `loadSeriesRetentionHours` (default 6, the intention); by ROWS,
|
|
13
|
+
* DEFAULT_MAX_ROWS (the guarantee, which bites regardless of the age knob).
|
|
14
|
+
* Both prune oldest-first through a bounded page — never a full scan — and
|
|
15
|
+
* the row cap logs when it bites. Losing the table loses the cold window
|
|
16
|
+
* only: the ring keeps serving the last hour and the charts keep drawing."
|
|
17
|
+
*/
|
|
18
|
+
export declare const LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
|
|
19
|
+
/**
|
|
20
|
+
* Bytes one stored row costs on disk, MEASURED — table plus index, after a
|
|
21
|
+
* truncating checkpoint. Used to project the cost of a configuration for the
|
|
22
|
+
* operator BEFORE they apply it, so the knob is honest.
|
|
23
|
+
*/
|
|
24
|
+
export declare const MEASURED_BYTES_PER_ROW = 87;
|
|
25
|
+
/**
|
|
26
|
+
* Sampling cadence bounds, in seconds. Outside this range a cadence is
|
|
27
|
+
* REFUSED — see {@link resolveLoadSeriesConfig}.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MIN_CADENCE_SEC = 5;
|
|
30
|
+
export declare const MAX_CADENCE_SEC = 60;
|
|
31
|
+
export declare const DEFAULT_CADENCE_SEC = 10;
|
|
32
|
+
/** Retention the operator asked for, in hours. Six is the shipped default. */
|
|
33
|
+
export declare const DEFAULT_RETENTION_HOURS = 6;
|
|
34
|
+
export declare const MIN_RETENTION_HOURS = 1;
|
|
35
|
+
export declare const MAX_RETENTION_HOURS = 72;
|
|
36
|
+
/**
|
|
37
|
+
* Hard ceiling on the table, in ROWS.
|
|
38
|
+
*
|
|
39
|
+
* The guarantee behind the retention knob. Sized against the shipped default
|
|
40
|
+
* and against the worst configuration the knob can express:
|
|
41
|
+
*
|
|
42
|
+
* | cadence | retention | rows (3 nodes, 76 processes) |
|
|
43
|
+
* | --- | --- | --- |
|
|
44
|
+
* | 10 s | 6 h (default) | 164 160 |
|
|
45
|
+
* | 5 s | 6 h | 328 320 |
|
|
46
|
+
* | 5 s | 72 h | 3 939 840 — capped here |
|
|
47
|
+
*
|
|
48
|
+
* 500 000 rows x the measured 87 B = **41.5 MiB**, roughly 3x what the shipped
|
|
49
|
+
* default needs and far below the 2 M rows a three-day retention would reach.
|
|
50
|
+
* It is deliberately NOT a knob: the point of a guarantee is that it is not
|
|
51
|
+
* subject to the same hand that raised the intention. This is the discipline
|
|
52
|
+
* the in-memory ring already adopted (`DEFAULT_MAX_TOTAL_PROCESS_ROWS`) and
|
|
53
|
+
* the one the RTP pre-roll rings still lack — a maximum per instance and no
|
|
54
|
+
* budget at all for the whole, which reached 898 MB across 47 rings
|
|
55
|
+
* (`docs/architecture/load-ledger.md` #5).
|
|
56
|
+
*/
|
|
57
|
+
export declare const DEFAULT_MAX_ROWS = 500000;
|
|
58
|
+
/**
|
|
59
|
+
* Rows a single prune pass may remove.
|
|
60
|
+
*
|
|
61
|
+
* The prune is BOUNDED, never a full scan. It reads at most this many keys
|
|
62
|
+
* oldest-first through `idx_load_samples_node_at`, takes the last one's `atMs`
|
|
63
|
+
* as the effective cutoff and issues ONE `deleteWhere` up to it. A backlog
|
|
64
|
+
* larger than a page is worked off across passes rather than in one stall —
|
|
65
|
+
* which is the defect corrected in the recorder the same week, where
|
|
66
|
+
* `accountingForLocations` visited 6.7 M rows twice a minute.
|
|
67
|
+
*/
|
|
68
|
+
export declare const PRUNE_PAGE_ROWS = 20000;
|
|
69
|
+
/** How often a prune pass may run. A bound, not a deadline. */
|
|
70
|
+
export declare const PRUNE_INTERVAL_MS = 60000;
|
|
71
|
+
export declare const LOAD_SERIES_COLUMNS: readonly [{
|
|
72
|
+
readonly name: "id";
|
|
73
|
+
readonly type: "INTEGER";
|
|
74
|
+
readonly primaryKey: true;
|
|
75
|
+
readonly notNull: true;
|
|
76
|
+
}, {
|
|
77
|
+
readonly name: "nodeId";
|
|
78
|
+
readonly type: "TEXT";
|
|
79
|
+
readonly notNull: true;
|
|
80
|
+
}, {
|
|
81
|
+
readonly name: "atMs";
|
|
82
|
+
readonly type: "INTEGER";
|
|
83
|
+
readonly notNull: true;
|
|
84
|
+
}, {
|
|
85
|
+
readonly name: "pid";
|
|
86
|
+
readonly type: "INTEGER";
|
|
87
|
+
readonly notNull: true;
|
|
88
|
+
}, {
|
|
89
|
+
readonly name: "addonId";
|
|
90
|
+
readonly type: "TEXT";
|
|
91
|
+
}, {
|
|
92
|
+
readonly name: "classification";
|
|
93
|
+
readonly type: "TEXT";
|
|
94
|
+
readonly notNull: true;
|
|
95
|
+
}, {
|
|
96
|
+
readonly name: "cpuDeci";
|
|
97
|
+
readonly type: "INTEGER";
|
|
98
|
+
readonly notNull: true;
|
|
99
|
+
}, {
|
|
100
|
+
readonly name: "rssMib";
|
|
101
|
+
readonly type: "INTEGER";
|
|
102
|
+
readonly notNull: true;
|
|
103
|
+
}, {
|
|
104
|
+
readonly name: "cpuMainDeci";
|
|
105
|
+
readonly type: "INTEGER";
|
|
106
|
+
}, {
|
|
107
|
+
readonly name: "cpuGcDeci";
|
|
108
|
+
readonly type: "INTEGER";
|
|
109
|
+
}];
|
|
110
|
+
export declare const LOAD_SERIES_INDEXES: readonly [{
|
|
111
|
+
readonly name: "idx_load_samples_node_at";
|
|
112
|
+
readonly columns: readonly ["nodeId", "atMs"];
|
|
113
|
+
}];
|
|
114
|
+
/** One stored process row, in the units the table stores them in. */
|
|
115
|
+
export interface LoadSeriesRow {
|
|
116
|
+
readonly nodeId: string;
|
|
117
|
+
readonly atMs: number;
|
|
118
|
+
readonly pid: number;
|
|
119
|
+
readonly addonId: string | null;
|
|
120
|
+
readonly classification: string;
|
|
121
|
+
readonly cpuDeci: number;
|
|
122
|
+
readonly rssMib: number;
|
|
123
|
+
readonly cpuMainDeci: number | null;
|
|
124
|
+
readonly cpuGcDeci: number | null;
|
|
125
|
+
}
|
|
126
|
+
/** The process shape both tiers hand back — the series' own row, not `ps`'s. */
|
|
127
|
+
export interface RetainedProcess {
|
|
128
|
+
readonly pid: number;
|
|
129
|
+
readonly addonId: string | null;
|
|
130
|
+
readonly classification: string;
|
|
131
|
+
readonly cpuPercent: number;
|
|
132
|
+
readonly memoryRssBytes: number;
|
|
133
|
+
readonly cpuMainPercent: number | null;
|
|
134
|
+
readonly cpuGcPercent: number | null;
|
|
135
|
+
}
|
|
136
|
+
/** One retained sample: when it was taken, and the processes in it. */
|
|
137
|
+
export interface RetainedSample {
|
|
138
|
+
readonly atMs: number;
|
|
139
|
+
readonly processes: readonly RetainedProcess[];
|
|
140
|
+
}
|
|
141
|
+
/** Storage row → the shape a reader gets. The ONE decode. */
|
|
142
|
+
export declare function rowToProcess(row: LoadSeriesRow): RetainedProcess;
|
|
143
|
+
/** The shape a reader gets → the storage row. The ONE encode. */
|
|
144
|
+
export declare function processToRow(nodeId: string, atMs: number, process: RetainedProcess): LoadSeriesRow;
|
|
145
|
+
/** Group flat rows into samples, oldest-first. A sample is one `atMs`. */
|
|
146
|
+
export declare function rowsToSamples(rows: readonly LoadSeriesRow[]): readonly RetainedSample[];
|
|
147
|
+
/** The two bounds in force. */
|
|
148
|
+
export interface LoadSeriesRetention {
|
|
149
|
+
readonly retentionHours: number;
|
|
150
|
+
readonly maxRows: number;
|
|
151
|
+
}
|
|
152
|
+
export interface LoadSeriesStoreDeps {
|
|
153
|
+
readonly store: SettingsStoreClient;
|
|
154
|
+
readonly logger: IScopedLogger;
|
|
155
|
+
readonly now?: () => number;
|
|
156
|
+
readonly prunePageRows?: number;
|
|
157
|
+
readonly pruneIntervalMs?: number;
|
|
158
|
+
}
|
|
159
|
+
/** What one prune pass did. Returned so a test can assert the bound held. */
|
|
160
|
+
export interface PruneOutcome {
|
|
161
|
+
readonly deletedByAge: number;
|
|
162
|
+
readonly deletedByCap: number;
|
|
163
|
+
/** Rows read to find the cutoffs — the number that must stay bounded. */
|
|
164
|
+
readonly rowsExamined: number;
|
|
165
|
+
readonly capBit: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The cold tier. Append-only, bounded, and never the authority on anything the
|
|
169
|
+
* hot ring can answer.
|
|
170
|
+
*
|
|
171
|
+
* Every method is best-effort and says so in the log rather than throwing at
|
|
172
|
+
* the emitter: a storage hiccup must cost a gap in a chart, never a dropped
|
|
173
|
+
* snapshot on the bus.
|
|
174
|
+
*/
|
|
175
|
+
export declare class LoadSeriesStore {
|
|
176
|
+
private declared;
|
|
177
|
+
private lastPruneAtMs;
|
|
178
|
+
private readonly store;
|
|
179
|
+
private readonly logger;
|
|
180
|
+
private readonly nowFn;
|
|
181
|
+
private readonly prunePageRows;
|
|
182
|
+
private readonly pruneIntervalMs;
|
|
183
|
+
constructor(deps: LoadSeriesStoreDeps);
|
|
184
|
+
/** Idempotently declare the collection. `false` when the store refused. */
|
|
185
|
+
declare(): Promise<boolean>;
|
|
186
|
+
/**
|
|
187
|
+
* Append ONE sample — every process row of it — in ONE transaction.
|
|
188
|
+
*
|
|
189
|
+
* Never a write per row. `insertMany` exists for exactly this: at the 10 s
|
|
190
|
+
* default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
|
|
191
|
+
* on the connection that also serves every cluster-wide configuration read
|
|
192
|
+
* is a constant load nobody asked for.
|
|
193
|
+
*/
|
|
194
|
+
append(nodeId: string, atMs: number, processes: readonly RetainedProcess[]): Promise<number>;
|
|
195
|
+
/**
|
|
196
|
+
* Read one node's cold samples, oldest-first.
|
|
197
|
+
*
|
|
198
|
+
* `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
|
|
199
|
+
* timestamp it holds gets only what it is missing. `limitRows` bounds the
|
|
200
|
+
* read in ROWS (not samples) because rows are what the query costs.
|
|
201
|
+
*/
|
|
202
|
+
read(nodeId: string, sinceMs: number, limitRows: number): Promise<readonly RetainedSample[]>;
|
|
203
|
+
/**
|
|
204
|
+
* Enforce BOTH bounds, oldest-first, through a bounded page each.
|
|
205
|
+
*
|
|
206
|
+
* Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
|
|
207
|
+
* append path must not pay a sweep on every sample.
|
|
208
|
+
*/
|
|
209
|
+
prune(nodeIds: readonly string[], retention: LoadSeriesRetention, force?: boolean): Promise<PruneOutcome | null>;
|
|
210
|
+
/** Total rows, or `null` when the store could not answer. */
|
|
211
|
+
count(): Promise<number | null>;
|
|
212
|
+
/**
|
|
213
|
+
* Delete this node's rows older than `cutoff`, at most one page's worth.
|
|
214
|
+
*
|
|
215
|
+
* The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
|
|
216
|
+
* own is one statement but an UNBOUNDED one — a first pass after a retention
|
|
217
|
+
* change would delete millions of rows inside a single stalling transaction.
|
|
218
|
+
* So the page is read first (keys only, ordered by the index), its last
|
|
219
|
+
* `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
|
|
220
|
+
*/
|
|
221
|
+
private pruneNodeToCutoff;
|
|
222
|
+
/**
|
|
223
|
+
* Drop the oldest rows across the known nodes until `excess` is covered.
|
|
224
|
+
*
|
|
225
|
+
* Same bounded-page technique, walked per node so the one index serves it.
|
|
226
|
+
* The node holding the oldest rows pays first, which is also the node
|
|
227
|
+
* producing the pressure when a runaway process count is the cause.
|
|
228
|
+
*/
|
|
229
|
+
private pruneOldestRows;
|
|
230
|
+
}
|
|
231
|
+
/** Structural validation on read. A malformed row is skipped, never charted. */
|
|
232
|
+
export declare function recordToRow(data: Record<string, unknown>): LoadSeriesRow | null;
|