@camstack/system 1.2.381 → 1.2.383
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 +4 -2
- package/dist/addon-runner.mjs +4 -2
- package/dist/addon-utils.js +1 -1
- package/dist/addon-utils.mjs +1 -1
- package/dist/auth/admin-token-manager.d.ts +75 -0
- package/dist/auth/api-key-manager.d.ts +1 -14
- package/dist/auth/auth-audit-log.d.ts +59 -0
- package/dist/auth/auth-manager.d.ts +28 -16
- package/dist/auth/cidr.d.ts +22 -0
- package/dist/auth/login-session-manager.d.ts +158 -0
- package/dist/auth/user-management-internal.d.ts +69 -0
- 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/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +14 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +291 -3631
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +284 -3617
- package/dist/builtins/backup-orchestrator/encryption-key-store.d.ts +36 -0
- package/dist/builtins/backup-orchestrator/manifest-store.d.ts +19 -1
- package/dist/builtins/backup-orchestrator/restore-archive.d.ts +24 -0
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- 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 +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- 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/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +1110 -735
- package/dist/builtins/keychain/index.mjs +1109 -734
- package/dist/builtins/keychain/keychain-reset-trigger.d.ts +25 -0
- package/dist/builtins/keychain/keychain-service.d.ts +36 -3
- package/dist/builtins/keychain/keychain-store.d.ts +24 -1
- package/dist/builtins/keychain/system-held-secrets.d.ts +2 -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/local-auth.addon.d.ts +16 -0
- package/dist/builtins/local-auth/local-auth.addon.js +1028 -294
- package/dist/builtins/local-auth/local-auth.addon.mjs +1035 -299
- package/dist/builtins/local-network/local-network.addon.js +3 -3
- package/dist/builtins/local-network/local-network.addon.mjs +3 -3
- package/dist/builtins/loki-logging/index.js +97 -29
- package/dist/builtins/loki-logging/index.mjs +97 -29
- package/dist/builtins/loki-logging/loki-logging.addon.d.ts +7 -23
- package/dist/builtins/loki-logging/retire-enabled-flag.d.ts +23 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- 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.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 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +2 -2
- package/dist/builtins/system-backup/backup-encryption.d.ts +57 -0
- package/dist/builtins/system-backup/system-backup.service.d.ts +16 -1
- package/dist/builtins/system-config/cluster-secret-keeper.d.ts +7 -0
- package/dist/builtins/system-config/index.d.ts +1 -0
- package/dist/builtins/system-config/index.js +2 -0
- package/dist/builtins/system-config/index.mjs +2 -2
- package/dist/builtins/system-config/mesh-actions.d.ts +19 -0
- package/dist/builtins/system-config/system-config.addon.d.ts +47 -4
- package/dist/builtins/system-config/system-config.addon.js +87858 -43
- package/dist/builtins/system-config/system-config.addon.mjs +87813 -39
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-hf-Gce_4.js → child-cap-dispatch-BD8fhJ1b.js} +7 -4
- package/dist/{child-cap-dispatch-BTe4eKep.mjs → child-cap-dispatch-C68N2a6Y.mjs} +11 -8
- package/dist/{composition-sources-4SmsP0O4.js → composition-sources-ChpT_Yw4.js} +1 -1
- package/dist/{composition-sources-CgOQVIwa.mjs → composition-sources-DM7Ft_tp.mjs} +1 -1
- package/dist/{cluster-secret-BUiG8DCo.js → constants-CDpry1_T.js} +82 -1
- package/dist/{cluster-secret-DgJTZ4qY.mjs → constants-e3mCa1jG.mjs} +23 -2
- package/dist/{dist-BugrtARh.js → dist-CUNGH0fs.js} +1755 -157
- package/dist/{dist-BC0GnGeg.mjs → dist-osl3bkF0.mjs} +1732 -158
- package/dist/{event-loop-stall-monitor-GgpnfAC0.mjs → event-loop-stall-monitor-BeeWMnyT.mjs} +15 -15
- package/dist/{event-loop-stall-monitor-CnQ-K6Gn.js → event-loop-stall-monitor-Dk28EF8B.js} +15 -15
- package/dist/{hub-cap-forward-CH6_VWVO.mjs → hub-cap-forward-CDiN1p98.mjs} +18 -2
- package/dist/{hub-cap-forward-COCmYaup.js → hub-cap-forward-W8oEm4ME.js} +23 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +1023 -83525
- package/dist/index.mjs +1218 -83794
- package/dist/{jwt-signing-key-CsuVKA1W.mjs → jwt-signing-key-MQLxLfgF.mjs} +1 -1
- package/dist/{jwt-signing-key-CjoZcol8.js → jwt-signing-key-WI6z7JBo.js} +1 -1
- package/dist/kernel/index.d.ts +17 -1
- package/dist/kernel/moleculer/agent-mesh-authenticator.d.ts +41 -0
- package/dist/kernel/moleculer/broker-factory.d.ts +25 -2
- package/dist/kernel/moleculer/child-addon-call-dispatch.d.ts +1 -0
- package/dist/kernel/moleculer/hub-service.d.ts +9 -0
- package/dist/kernel/moleculer/mesh-access-middleware.d.ts +33 -0
- package/dist/kernel/moleculer/mesh-auth-mode.d.ts +64 -0
- package/dist/kernel/moleculer/mesh-authenticator.d.ts +71 -0
- package/dist/kernel/moleculer/mesh-auto-enforce.d.ts +26 -0
- package/dist/kernel/moleculer/mesh-join-client.d.ts +51 -0
- package/dist/kernel/moleculer/mesh-join-proof.d.ts +39 -0
- package/dist/kernel/moleculer/mesh-mtls/constants.d.ts +20 -0
- package/dist/kernel/moleculer/mesh-mtls/index.d.ts +9 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-ca-keeper.d.ts +55 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-ca.d.ts +45 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-crl.d.ts +16 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-enroll.d.ts +27 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-join-token.d.ts +55 -0
- package/dist/kernel/moleculer/mesh-mtls/mesh-tls-enabler.d.ts +5 -0
- package/dist/kernel/moleculer/mesh-mtls/tls-tcp-transporter.d.ts +92 -0
- package/dist/kernel/system-settings-policy.d.ts +4 -3
- package/dist/kernel/transport/hub-cap-forward.d.ts +14 -3
- package/dist/kernel/transport/index.d.ts +1 -1
- package/dist/ms-DlIWNkkk.mjs +160 -0
- package/dist/ms-t68_FaqE.js +171 -0
- package/dist/restore-archive-C5f9yX90.js +4139 -0
- package/dist/restore-archive-CZRtI1k9.mjs +4045 -0
- package/dist/{retired-settings-keys-MCVhxPkJ.mjs → retired-settings-keys-LuDd4y3M.mjs} +1 -1
- package/dist/{retired-settings-keys-DCuetwOt.js → retired-settings-keys-WLrbzdPR.js} +1 -1
- package/dist/tls-BWpwU-d5.js +1160 -0
- package/dist/tls-C2NT-Orr.mjs +949 -0
- package/dist/{tls-CCMX7MjX.js → x509.es-BhTzcFhZ.js} +65 -1108
- package/dist/{tls-CJHogwTl.mjs → x509.es-Xf3K_brW.mjs} +40 -950
- package/package.json +3 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { Vn as userManagementCapability, W as UserRecordSchema, Y as authProviderCapability, r as ApiKeyRecordSchema, ur as BaseAddon, z as ScopedTokenSchema } from "../../dist-osl3bkF0.mjs";
|
|
3
|
+
import { n as require_safe_buffer, t as require_ms } from "../../ms-DlIWNkkk.mjs";
|
|
4
|
+
import { a as loadJwtSigningKey, i as getProcessJwtSigningKeyState, o as processJwtSigningKey, s as setProcessJwtSigningKey, t as JWT_SIGNING_KEY_REF } from "../../jwt-signing-key-MQLxLfgF.mjs";
|
|
5
|
+
import * as crypto$1 from "node:crypto";
|
|
5
6
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
7
|
import nodeCrypto from "crypto";
|
|
7
8
|
import * as path$1 from "node:path";
|
|
8
9
|
import * as fs from "node:fs";
|
|
9
10
|
//#region src/auth/parse-record.ts
|
|
10
|
-
function parseRecord(kind, schema, data) {
|
|
11
|
+
function parseRecord$1(kind, schema, data) {
|
|
11
12
|
try {
|
|
12
13
|
return schema.parse(data);
|
|
13
14
|
} catch (err) {
|
|
@@ -19,64 +20,26 @@ function parseRecord(kind, schema, data) {
|
|
|
19
20
|
}
|
|
20
21
|
//#endregion
|
|
21
22
|
//#region src/auth/api-key-manager.ts
|
|
23
|
+
/**
|
|
24
|
+
* Legacy API keys — LIST and REVOKE only.
|
|
25
|
+
*
|
|
26
|
+
* No boundary accepts one (the tRPC context has no branch for them) and
|
|
27
|
+
* nothing creates them any more: what is left is the rows a hub may still
|
|
28
|
+
* hold, so an operator can see and delete them. Scoped tokens (`cst_`) are
|
|
29
|
+
* the credential for scripts and integrations.
|
|
30
|
+
*/
|
|
22
31
|
var API_KEYS_COLLECTION$1 = "api_keys";
|
|
23
32
|
function parseApiKey(data) {
|
|
24
|
-
return parseRecord("api-key", ApiKeyRecordSchema, data);
|
|
33
|
+
return parseRecord$1("api-key", ApiKeyRecordSchema, data);
|
|
25
34
|
}
|
|
26
35
|
var ApiKeyManager = class {
|
|
27
36
|
storageAccess;
|
|
28
|
-
|
|
29
|
-
constructor(storageAccess, auth) {
|
|
37
|
+
constructor(storageAccess) {
|
|
30
38
|
this.storageAccess = storageAccess;
|
|
31
|
-
this.auth = auth;
|
|
32
39
|
}
|
|
33
40
|
get store() {
|
|
34
41
|
return this.storageAccess.getStore();
|
|
35
42
|
}
|
|
36
|
-
async create(input) {
|
|
37
|
-
const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
|
|
38
|
-
const now = Date.now();
|
|
39
|
-
const record = {
|
|
40
|
-
id: crypto$2.randomUUID(),
|
|
41
|
-
label: input.label,
|
|
42
|
-
isAdmin: input.isAdmin ?? false,
|
|
43
|
-
allowedProviders: input.allowedProviders ?? "*",
|
|
44
|
-
allowedDevices: input.allowedDevices ?? {},
|
|
45
|
-
tokenHash: hash,
|
|
46
|
-
tokenPrefix: prefix,
|
|
47
|
-
createdAt: now
|
|
48
|
-
};
|
|
49
|
-
await this.store.insert.mutate({
|
|
50
|
-
collection: API_KEYS_COLLECTION$1,
|
|
51
|
-
record: {
|
|
52
|
-
id: record.id,
|
|
53
|
-
data: { ...record }
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return {
|
|
57
|
-
record,
|
|
58
|
-
token: rawToken
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
async validateToken(token) {
|
|
62
|
-
const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
|
|
63
|
-
for (const entry of allKeys) {
|
|
64
|
-
const record = parseApiKey(entry.data);
|
|
65
|
-
if (this.auth.validateApiKey(token, record.tokenHash)) {
|
|
66
|
-
const updatedData = {
|
|
67
|
-
...record,
|
|
68
|
-
lastUsedAt: Date.now()
|
|
69
|
-
};
|
|
70
|
-
await this.store.update.mutate({
|
|
71
|
-
collection: API_KEYS_COLLECTION$1,
|
|
72
|
-
id: record.id,
|
|
73
|
-
data: { ...updatedData }
|
|
74
|
-
});
|
|
75
|
-
return updatedData;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
43
|
async listAll() {
|
|
81
44
|
return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
|
|
82
45
|
const { tokenHash: _th, ...rest } = parseApiKey(r.data);
|
|
@@ -189,7 +152,7 @@ function genSalt(rounds, seed_length, callback) {
|
|
|
189
152
|
* @returns {!Promise} If `callback` has been omitted
|
|
190
153
|
* @throws {Error} If `callback` is present but not a function
|
|
191
154
|
*/
|
|
192
|
-
function hash(password, salt, callback, progressCallback) {
|
|
155
|
+
function hash$1(password, salt, callback, progressCallback) {
|
|
193
156
|
function _async(callback) {
|
|
194
157
|
if (typeof password === "string" && typeof salt === "number") genSalt(salt, function(err, salt) {
|
|
195
158
|
_hash(password, salt, callback, progressCallback);
|
|
@@ -242,7 +205,7 @@ function compare(password, hashValue, callback, progressCallback) {
|
|
|
242
205
|
nextTick(callback.bind(this, null, false));
|
|
243
206
|
return;
|
|
244
207
|
}
|
|
245
|
-
hash(password, hashValue.substring(0, 29), function(err, comp) {
|
|
208
|
+
hash$1(password, hashValue.substring(0, 29), function(err, comp) {
|
|
246
209
|
if (err) callback(err);
|
|
247
210
|
else callback(null, safeStringCompare(comp, hashValue));
|
|
248
211
|
}, progressCallback);
|
|
@@ -1890,46 +1853,6 @@ function _hash(password, salt, callback, progressCallback) {
|
|
|
1890
1853
|
}, progressCallback);
|
|
1891
1854
|
}
|
|
1892
1855
|
//#endregion
|
|
1893
|
-
//#region ../../node_modules/safe-buffer/index.js
|
|
1894
|
-
var require_safe_buffer = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1895
|
-
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
1896
|
-
var buffer = __require("buffer");
|
|
1897
|
-
var Buffer = buffer.Buffer;
|
|
1898
|
-
function copyProps(src, dst) {
|
|
1899
|
-
for (var key in src) dst[key] = src[key];
|
|
1900
|
-
}
|
|
1901
|
-
if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) module.exports = buffer;
|
|
1902
|
-
else {
|
|
1903
|
-
copyProps(buffer, exports);
|
|
1904
|
-
exports.Buffer = SafeBuffer;
|
|
1905
|
-
}
|
|
1906
|
-
function SafeBuffer(arg, encodingOrOffset, length) {
|
|
1907
|
-
return Buffer(arg, encodingOrOffset, length);
|
|
1908
|
-
}
|
|
1909
|
-
SafeBuffer.prototype = Object.create(Buffer.prototype);
|
|
1910
|
-
copyProps(Buffer, SafeBuffer);
|
|
1911
|
-
SafeBuffer.from = function(arg, encodingOrOffset, length) {
|
|
1912
|
-
if (typeof arg === "number") throw new TypeError("Argument must not be a number");
|
|
1913
|
-
return Buffer(arg, encodingOrOffset, length);
|
|
1914
|
-
};
|
|
1915
|
-
SafeBuffer.alloc = function(size, fill, encoding) {
|
|
1916
|
-
if (typeof size !== "number") throw new TypeError("Argument must be a number");
|
|
1917
|
-
var buf = Buffer(size);
|
|
1918
|
-
if (fill !== void 0) if (typeof encoding === "string") buf.fill(fill, encoding);
|
|
1919
|
-
else buf.fill(fill);
|
|
1920
|
-
else buf.fill(0);
|
|
1921
|
-
return buf;
|
|
1922
|
-
};
|
|
1923
|
-
SafeBuffer.allocUnsafe = function(size) {
|
|
1924
|
-
if (typeof size !== "number") throw new TypeError("Argument must be a number");
|
|
1925
|
-
return Buffer(size);
|
|
1926
|
-
};
|
|
1927
|
-
SafeBuffer.allocUnsafeSlow = function(size) {
|
|
1928
|
-
if (typeof size !== "number") throw new TypeError("Argument must be a number");
|
|
1929
|
-
return buffer.SlowBuffer(size);
|
|
1930
|
-
};
|
|
1931
|
-
}));
|
|
1932
|
-
//#endregion
|
|
1933
1856
|
//#region ../../node_modules/jws/lib/data-stream.js
|
|
1934
1857
|
var require_data_stream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
1935
1858
|
var Buffer = require_safe_buffer().Buffer;
|
|
@@ -2113,14 +2036,14 @@ var require_buffer_equal_constant_time = /* @__PURE__ */ __commonJSMin(((exports
|
|
|
2113
2036
|
//#region ../../node_modules/jwa/index.js
|
|
2114
2037
|
var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2115
2038
|
var Buffer = require_safe_buffer().Buffer;
|
|
2116
|
-
var crypto$
|
|
2039
|
+
var crypto$2 = __require("crypto");
|
|
2117
2040
|
var formatEcdsa = require_ecdsa_sig_formatter();
|
|
2118
2041
|
var util$2 = __require("util");
|
|
2119
2042
|
var MSG_INVALID_ALGORITHM = "\"%s\" is not a valid algorithm.\n Supported algorithms are:\n \"HS256\", \"HS384\", \"HS512\", \"RS256\", \"RS384\", \"RS512\", \"PS256\", \"PS384\", \"PS512\", \"ES256\", \"ES384\", \"ES512\" and \"none\".";
|
|
2120
2043
|
var MSG_INVALID_SECRET = "secret must be a string or buffer";
|
|
2121
2044
|
var MSG_INVALID_VERIFIER_KEY = "key must be a string or a buffer";
|
|
2122
2045
|
var MSG_INVALID_SIGNER_KEY = "key must be a string, a buffer or an object";
|
|
2123
|
-
var supportsKeyObjects = typeof crypto$
|
|
2046
|
+
var supportsKeyObjects = typeof crypto$2.createPublicKey === "function";
|
|
2124
2047
|
if (supportsKeyObjects) {
|
|
2125
2048
|
MSG_INVALID_VERIFIER_KEY += " or a KeyObject";
|
|
2126
2049
|
MSG_INVALID_SECRET += "or a KeyObject";
|
|
@@ -2173,14 +2096,14 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2173
2096
|
return function sign(thing, secret) {
|
|
2174
2097
|
checkIsSecretKey(secret);
|
|
2175
2098
|
thing = normalizeInput(thing);
|
|
2176
|
-
var hmac = crypto$
|
|
2099
|
+
var hmac = crypto$2.createHmac("sha" + bits, secret);
|
|
2177
2100
|
return fromBase64((hmac.update(thing), hmac.digest("base64")));
|
|
2178
2101
|
};
|
|
2179
2102
|
}
|
|
2180
2103
|
var bufferEqual;
|
|
2181
|
-
var timingSafeEqual = "timingSafeEqual" in crypto$
|
|
2104
|
+
var timingSafeEqual = "timingSafeEqual" in crypto$2 ? function timingSafeEqual(a, b) {
|
|
2182
2105
|
if (a.byteLength !== b.byteLength) return false;
|
|
2183
|
-
return crypto$
|
|
2106
|
+
return crypto$2.timingSafeEqual(a, b);
|
|
2184
2107
|
} : function timingSafeEqual(a, b) {
|
|
2185
2108
|
if (!bufferEqual) bufferEqual = require_buffer_equal_constant_time();
|
|
2186
2109
|
return bufferEqual(a, b);
|
|
@@ -2195,7 +2118,7 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2195
2118
|
return function sign(thing, privateKey) {
|
|
2196
2119
|
checkIsPrivateKey(privateKey);
|
|
2197
2120
|
thing = normalizeInput(thing);
|
|
2198
|
-
var signer = crypto$
|
|
2121
|
+
var signer = crypto$2.createSign("RSA-SHA" + bits);
|
|
2199
2122
|
return fromBase64((signer.update(thing), signer.sign(privateKey, "base64")));
|
|
2200
2123
|
};
|
|
2201
2124
|
}
|
|
@@ -2204,7 +2127,7 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2204
2127
|
checkIsPublicKey(publicKey);
|
|
2205
2128
|
thing = normalizeInput(thing);
|
|
2206
2129
|
signature = toBase64(signature);
|
|
2207
|
-
var verifier = crypto$
|
|
2130
|
+
var verifier = crypto$2.createVerify("RSA-SHA" + bits);
|
|
2208
2131
|
verifier.update(thing);
|
|
2209
2132
|
return verifier.verify(publicKey, signature, "base64");
|
|
2210
2133
|
};
|
|
@@ -2213,11 +2136,11 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2213
2136
|
return function sign(thing, privateKey) {
|
|
2214
2137
|
checkIsPrivateKey(privateKey);
|
|
2215
2138
|
thing = normalizeInput(thing);
|
|
2216
|
-
var signer = crypto$
|
|
2139
|
+
var signer = crypto$2.createSign("RSA-SHA" + bits);
|
|
2217
2140
|
return fromBase64((signer.update(thing), signer.sign({
|
|
2218
2141
|
key: privateKey,
|
|
2219
|
-
padding: crypto$
|
|
2220
|
-
saltLength: crypto$
|
|
2142
|
+
padding: crypto$2.constants.RSA_PKCS1_PSS_PADDING,
|
|
2143
|
+
saltLength: crypto$2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
2221
2144
|
}, "base64")));
|
|
2222
2145
|
};
|
|
2223
2146
|
}
|
|
@@ -2226,12 +2149,12 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2226
2149
|
checkIsPublicKey(publicKey);
|
|
2227
2150
|
thing = normalizeInput(thing);
|
|
2228
2151
|
signature = toBase64(signature);
|
|
2229
|
-
var verifier = crypto$
|
|
2152
|
+
var verifier = crypto$2.createVerify("RSA-SHA" + bits);
|
|
2230
2153
|
verifier.update(thing);
|
|
2231
2154
|
return verifier.verify({
|
|
2232
2155
|
key: publicKey,
|
|
2233
|
-
padding: crypto$
|
|
2234
|
-
saltLength: crypto$
|
|
2156
|
+
padding: crypto$2.constants.RSA_PKCS1_PSS_PADDING,
|
|
2157
|
+
saltLength: crypto$2.constants.RSA_PSS_SALTLEN_DIGEST
|
|
2235
2158
|
}, signature, "base64");
|
|
2236
2159
|
};
|
|
2237
2160
|
}
|
|
@@ -2560,124 +2483,6 @@ var require_TokenExpiredError = /* @__PURE__ */ __commonJSMin(((exports, module)
|
|
|
2560
2483
|
module.exports = TokenExpiredError;
|
|
2561
2484
|
}));
|
|
2562
2485
|
//#endregion
|
|
2563
|
-
//#region ../../node_modules/ms/index.js
|
|
2564
|
-
var require_ms = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2565
|
-
/**
|
|
2566
|
-
* Helpers.
|
|
2567
|
-
*/
|
|
2568
|
-
var s = 1e3;
|
|
2569
|
-
var m = s * 60;
|
|
2570
|
-
var h = m * 60;
|
|
2571
|
-
var d = h * 24;
|
|
2572
|
-
var w = d * 7;
|
|
2573
|
-
var y = d * 365.25;
|
|
2574
|
-
/**
|
|
2575
|
-
* Parse or format the given `val`.
|
|
2576
|
-
*
|
|
2577
|
-
* Options:
|
|
2578
|
-
*
|
|
2579
|
-
* - `long` verbose formatting [false]
|
|
2580
|
-
*
|
|
2581
|
-
* @param {String|Number} val
|
|
2582
|
-
* @param {Object} [options]
|
|
2583
|
-
* @throws {Error} throw an error if val is not a non-empty string or a number
|
|
2584
|
-
* @return {String|Number}
|
|
2585
|
-
* @api public
|
|
2586
|
-
*/
|
|
2587
|
-
module.exports = function(val, options) {
|
|
2588
|
-
options = options || {};
|
|
2589
|
-
var type = typeof val;
|
|
2590
|
-
if (type === "string" && val.length > 0) return parse(val);
|
|
2591
|
-
else if (type === "number" && isFinite(val)) return options.long ? fmtLong(val) : fmtShort(val);
|
|
2592
|
-
throw new Error("val is not a non-empty string or a valid number. val=" + JSON.stringify(val));
|
|
2593
|
-
};
|
|
2594
|
-
/**
|
|
2595
|
-
* Parse the given `str` and return milliseconds.
|
|
2596
|
-
*
|
|
2597
|
-
* @param {String} str
|
|
2598
|
-
* @return {Number}
|
|
2599
|
-
* @api private
|
|
2600
|
-
*/
|
|
2601
|
-
function parse(str) {
|
|
2602
|
-
str = String(str);
|
|
2603
|
-
if (str.length > 100) return;
|
|
2604
|
-
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(str);
|
|
2605
|
-
if (!match) return;
|
|
2606
|
-
var n = parseFloat(match[1]);
|
|
2607
|
-
switch ((match[2] || "ms").toLowerCase()) {
|
|
2608
|
-
case "years":
|
|
2609
|
-
case "year":
|
|
2610
|
-
case "yrs":
|
|
2611
|
-
case "yr":
|
|
2612
|
-
case "y": return n * y;
|
|
2613
|
-
case "weeks":
|
|
2614
|
-
case "week":
|
|
2615
|
-
case "w": return n * w;
|
|
2616
|
-
case "days":
|
|
2617
|
-
case "day":
|
|
2618
|
-
case "d": return n * d;
|
|
2619
|
-
case "hours":
|
|
2620
|
-
case "hour":
|
|
2621
|
-
case "hrs":
|
|
2622
|
-
case "hr":
|
|
2623
|
-
case "h": return n * h;
|
|
2624
|
-
case "minutes":
|
|
2625
|
-
case "minute":
|
|
2626
|
-
case "mins":
|
|
2627
|
-
case "min":
|
|
2628
|
-
case "m": return n * m;
|
|
2629
|
-
case "seconds":
|
|
2630
|
-
case "second":
|
|
2631
|
-
case "secs":
|
|
2632
|
-
case "sec":
|
|
2633
|
-
case "s": return n * s;
|
|
2634
|
-
case "milliseconds":
|
|
2635
|
-
case "millisecond":
|
|
2636
|
-
case "msecs":
|
|
2637
|
-
case "msec":
|
|
2638
|
-
case "ms": return n;
|
|
2639
|
-
default: return;
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
/**
|
|
2643
|
-
* Short format for `ms`.
|
|
2644
|
-
*
|
|
2645
|
-
* @param {Number} ms
|
|
2646
|
-
* @return {String}
|
|
2647
|
-
* @api private
|
|
2648
|
-
*/
|
|
2649
|
-
function fmtShort(ms) {
|
|
2650
|
-
var msAbs = Math.abs(ms);
|
|
2651
|
-
if (msAbs >= d) return Math.round(ms / d) + "d";
|
|
2652
|
-
if (msAbs >= h) return Math.round(ms / h) + "h";
|
|
2653
|
-
if (msAbs >= m) return Math.round(ms / m) + "m";
|
|
2654
|
-
if (msAbs >= s) return Math.round(ms / s) + "s";
|
|
2655
|
-
return ms + "ms";
|
|
2656
|
-
}
|
|
2657
|
-
/**
|
|
2658
|
-
* Long format for `ms`.
|
|
2659
|
-
*
|
|
2660
|
-
* @param {Number} ms
|
|
2661
|
-
* @return {String}
|
|
2662
|
-
* @api private
|
|
2663
|
-
*/
|
|
2664
|
-
function fmtLong(ms) {
|
|
2665
|
-
var msAbs = Math.abs(ms);
|
|
2666
|
-
if (msAbs >= d) return plural(ms, msAbs, d, "day");
|
|
2667
|
-
if (msAbs >= h) return plural(ms, msAbs, h, "hour");
|
|
2668
|
-
if (msAbs >= m) return plural(ms, msAbs, m, "minute");
|
|
2669
|
-
if (msAbs >= s) return plural(ms, msAbs, s, "second");
|
|
2670
|
-
return ms + " ms";
|
|
2671
|
-
}
|
|
2672
|
-
/**
|
|
2673
|
-
* Pluralization helper.
|
|
2674
|
-
*/
|
|
2675
|
-
function plural(ms, msAbs, n, name) {
|
|
2676
|
-
var isPlural = msAbs >= n * 1.5;
|
|
2677
|
-
return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
|
|
2678
|
-
}
|
|
2679
|
-
}));
|
|
2680
|
-
//#endregion
|
|
2681
2486
|
//#region ../../node_modules/jsonwebtoken/lib/timespan.js
|
|
2682
2487
|
var require_timespan = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2683
2488
|
var ms = require_ms();
|
|
@@ -4156,7 +3961,7 @@ var require_verify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4156
3961
|
var validateAsymmetricKey = require_validateAsymmetricKey();
|
|
4157
3962
|
var PS_SUPPORTED = require_psSupported();
|
|
4158
3963
|
var jws = require_jws();
|
|
4159
|
-
var { KeyObject: KeyObject$1, createSecretKey: createSecretKey$1, createPublicKey } = __require("crypto");
|
|
3964
|
+
var { KeyObject: KeyObject$1, createSecretKey: createSecretKey$1, createPublicKey: createPublicKey$1 } = __require("crypto");
|
|
4160
3965
|
var PUB_KEY_ALGS = [
|
|
4161
3966
|
"RS256",
|
|
4162
3967
|
"RS384",
|
|
@@ -4224,7 +4029,7 @@ var require_verify = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4224
4029
|
if (hasSignature && !secretOrPublicKey) return done(new JsonWebTokenError("secret or public key must be provided"));
|
|
4225
4030
|
if (!hasSignature && !options.algorithms) return done(new JsonWebTokenError("please specify \"none\" in \"algorithms\" to verify unsigned tokens"));
|
|
4226
4031
|
if (secretOrPublicKey != null && !(secretOrPublicKey instanceof KeyObject$1)) try {
|
|
4227
|
-
secretOrPublicKey = createPublicKey(secretOrPublicKey);
|
|
4032
|
+
secretOrPublicKey = createPublicKey$1(secretOrPublicKey);
|
|
4228
4033
|
} catch (_) {
|
|
4229
4034
|
try {
|
|
4230
4035
|
secretOrPublicKey = createSecretKey$1(typeof secretOrPublicKey === "string" ? Buffer.from(secretOrPublicKey) : secretOrPublicKey);
|
|
@@ -6221,7 +6026,15 @@ var AuthManager = class {
|
|
|
6221
6026
|
get jwtSecret() {
|
|
6222
6027
|
return this.signingKey.current();
|
|
6223
6028
|
}
|
|
6224
|
-
|
|
6029
|
+
/**
|
|
6030
|
+
* @param options.expiresInSec an explicit lifetime (a session's access
|
|
6031
|
+
* token): wins over `auth.tokenExpiry`, bounded to at least one minute.
|
|
6032
|
+
*/
|
|
6033
|
+
signToken(payload, options = {}) {
|
|
6034
|
+
if (options.expiresInSec !== void 0) {
|
|
6035
|
+
const seconds = Math.max(60, Math.floor(options.expiresInSec));
|
|
6036
|
+
return import_jsonwebtoken.sign({ ...payload }, this.jwtSecret, { expiresIn: seconds });
|
|
6037
|
+
}
|
|
6225
6038
|
const configured = this.readConfiguredExpiry();
|
|
6226
6039
|
const parsed = parseTokenExpiry(configured);
|
|
6227
6040
|
if (!parsed.ok) {
|
|
@@ -6263,42 +6076,14 @@ var AuthManager = class {
|
|
|
6263
6076
|
return import_jsonwebtoken.verify(token, this.jwtSecret);
|
|
6264
6077
|
}
|
|
6265
6078
|
async hashPassword(password) {
|
|
6266
|
-
return hash(password, 10);
|
|
6079
|
+
return hash$1(password, 10);
|
|
6267
6080
|
}
|
|
6268
6081
|
async comparePassword(password, hash) {
|
|
6269
6082
|
return compare(password, hash);
|
|
6270
6083
|
}
|
|
6271
|
-
generateApiKey() {
|
|
6272
|
-
const token = crypto$2.randomBytes(32).toString("hex");
|
|
6273
|
-
return {
|
|
6274
|
-
token,
|
|
6275
|
-
hash: crypto$2.createHash("sha256").update(token).digest("hex"),
|
|
6276
|
-
prefix: token.slice(0, 8)
|
|
6277
|
-
};
|
|
6278
|
-
}
|
|
6279
|
-
validateApiKey(token, storedHash) {
|
|
6280
|
-
return crypto$2.createHash("sha256").update(token).digest("hex") === storedHash;
|
|
6281
|
-
}
|
|
6282
|
-
/**
|
|
6283
|
-
* Create a service token for agent/worker authentication.
|
|
6284
|
-
* Used when forking workers or when agents register.
|
|
6285
|
-
*/
|
|
6286
|
-
createServiceToken(opts) {
|
|
6287
|
-
const payload = {
|
|
6288
|
-
userId: opts.agentId,
|
|
6289
|
-
username: opts.agentId,
|
|
6290
|
-
isAdmin: true,
|
|
6291
|
-
type: "service",
|
|
6292
|
-
agentId: opts.agentId,
|
|
6293
|
-
allowedProviders: "*",
|
|
6294
|
-
allowedDevices: {}
|
|
6295
|
-
};
|
|
6296
|
-
const expiresIn = opts.expiresIn ?? "24h";
|
|
6297
|
-
return import_jsonwebtoken.sign(payload, this.jwtSecret, { expiresIn });
|
|
6298
|
-
}
|
|
6299
6084
|
/**
|
|
6300
6085
|
* Mint a short-lived HMAC-signed bridge token used by SSO-style auth
|
|
6301
|
-
* providers (OIDC, SAML,
|
|
6086
|
+
* providers (OIDC, SAML, …) to hand the post-callback
|
|
6302
6087
|
* claims to `/api/auth/sso/finish` without trusting unsigned query
|
|
6303
6088
|
* parameters. The endpoint verifies the token signature with the same
|
|
6304
6089
|
* `jwtSecret` and only then mints the user session JWT. This closes
|
|
@@ -6328,7 +6113,8 @@ var AuthManager = class {
|
|
|
6328
6113
|
...payload.sessionId !== void 0 ? { sessionId: payload.sessionId } : {},
|
|
6329
6114
|
...payload.refreshTtl !== void 0 ? { refreshTtl: payload.refreshTtl } : {},
|
|
6330
6115
|
...payload.linkIntent !== void 0 ? { linkIntent: payload.linkIntent } : {},
|
|
6331
|
-
...payload.issuer !== void 0 ? { issuer: payload.issuer } : {}
|
|
6116
|
+
...payload.issuer !== void 0 ? { issuer: payload.issuer } : {},
|
|
6117
|
+
...payload.browserBinding !== void 0 ? { browserBinding: payload.browserBinding } : {}
|
|
6332
6118
|
};
|
|
6333
6119
|
/**
|
|
6334
6120
|
* `'never'` mints with NO `exp` claim, and the option must be OMITTED to do
|
|
@@ -6372,6 +6158,7 @@ var AuthManager = class {
|
|
|
6372
6158
|
const sessionId = typeof decoded["sessionId"] === "string" ? decoded["sessionId"] : void 0;
|
|
6373
6159
|
const linkIntent = typeof decoded["linkIntent"] === "string" ? decoded["linkIntent"] : void 0;
|
|
6374
6160
|
const issuer = typeof decoded["issuer"] === "string" ? decoded["issuer"] : void 0;
|
|
6161
|
+
const browserBinding = typeof decoded["browserBinding"] === "string" ? decoded["browserBinding"] : void 0;
|
|
6375
6162
|
const rawRefreshTtl = decoded["refreshTtl"];
|
|
6376
6163
|
const refreshTtl = rawRefreshTtl === "never" || typeof rawRefreshTtl === "number" && rawRefreshTtl > 0 ? rawRefreshTtl : void 0;
|
|
6377
6164
|
return {
|
|
@@ -6389,7 +6176,8 @@ var AuthManager = class {
|
|
|
6389
6176
|
...sessionId !== void 0 ? { sessionId } : {},
|
|
6390
6177
|
...refreshTtl !== void 0 ? { refreshTtl } : {},
|
|
6391
6178
|
...linkIntent !== void 0 ? { linkIntent } : {},
|
|
6392
|
-
...issuer !== void 0 ? { issuer } : {}
|
|
6179
|
+
...issuer !== void 0 ? { issuer } : {},
|
|
6180
|
+
...browserBinding !== void 0 ? { browserBinding } : {}
|
|
6393
6181
|
};
|
|
6394
6182
|
} catch {
|
|
6395
6183
|
return null;
|
|
@@ -6415,10 +6203,43 @@ var AuthManager = class {
|
|
|
6415
6203
|
kind: "totp-challenge",
|
|
6416
6204
|
userId: payload.userId,
|
|
6417
6205
|
username: payload.username,
|
|
6418
|
-
isAdmin: payload.isAdmin
|
|
6206
|
+
isAdmin: payload.isAdmin,
|
|
6207
|
+
...payload.method !== void 0 ? { method: payload.method } : {}
|
|
6419
6208
|
}, this.jwtSecret, { expiresIn: ttlSec });
|
|
6420
6209
|
}
|
|
6421
6210
|
/**
|
|
6211
|
+
* A short-lived proof that the signed-in user just re-entered their password
|
|
6212
|
+
* or used a passkey. Sensitive self-service actions (linking a sign-in
|
|
6213
|
+
* account, disabling 2FA, removing a passkey, minting an admin token)
|
|
6214
|
+
* require one. `kind: 'reauth'` keeps it from ever being a bearer.
|
|
6215
|
+
*/
|
|
6216
|
+
signReauthToken(claims, ttlSec = 300) {
|
|
6217
|
+
return import_jsonwebtoken.sign({
|
|
6218
|
+
kind: "reauth",
|
|
6219
|
+
userId: claims.userId,
|
|
6220
|
+
method: claims.method,
|
|
6221
|
+
...claims.sessionId !== void 0 ? { sid: claims.sessionId } : {}
|
|
6222
|
+
}, this.jwtSecret, { expiresIn: ttlSec });
|
|
6223
|
+
}
|
|
6224
|
+
/** Decode a re-authentication proof, or `null` (forged, expired, another kind). */
|
|
6225
|
+
verifyReauthToken(token) {
|
|
6226
|
+
try {
|
|
6227
|
+
const decoded = import_jsonwebtoken.verify(token, this.jwtSecret);
|
|
6228
|
+
if (decoded["kind"] !== "reauth") return null;
|
|
6229
|
+
const userId = decoded["userId"];
|
|
6230
|
+
const method = decoded["method"];
|
|
6231
|
+
const sid = decoded["sid"];
|
|
6232
|
+
if (typeof userId !== "string" || method !== "password" && method !== "passkey") return null;
|
|
6233
|
+
return {
|
|
6234
|
+
userId,
|
|
6235
|
+
method,
|
|
6236
|
+
...typeof sid === "string" ? { sessionId: sid } : {}
|
|
6237
|
+
};
|
|
6238
|
+
} catch {
|
|
6239
|
+
return null;
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
/**
|
|
6422
6243
|
* Verify + decode a TOTP challenge token. Returns `null` for any
|
|
6423
6244
|
* failure (invalid signature, expired, wrong `kind`, missing
|
|
6424
6245
|
* claims). The caller MUST also verify the 6-digit code against
|
|
@@ -6433,10 +6254,12 @@ var AuthManager = class {
|
|
|
6433
6254
|
const username = decoded["username"];
|
|
6434
6255
|
const isAdmin = decoded["isAdmin"];
|
|
6435
6256
|
if (typeof userId !== "string" || typeof username !== "string" || typeof isAdmin !== "boolean") return null;
|
|
6257
|
+
const method = decoded["method"];
|
|
6436
6258
|
return {
|
|
6437
6259
|
userId,
|
|
6438
6260
|
username,
|
|
6439
|
-
isAdmin
|
|
6261
|
+
isAdmin,
|
|
6262
|
+
...method === "password" || method === "passkey" || method === "sso" ? { method } : {}
|
|
6440
6263
|
};
|
|
6441
6264
|
} catch {
|
|
6442
6265
|
return null;
|
|
@@ -6448,7 +6271,7 @@ var AuthManager = class {
|
|
|
6448
6271
|
var TOKENS_COLLECTION = "scoped_tokens";
|
|
6449
6272
|
var TOKEN_PREFIX = "cst_";
|
|
6450
6273
|
function parseToken(data) {
|
|
6451
|
-
return parseRecord("scoped-token", ScopedTokenSchema, data);
|
|
6274
|
+
return parseRecord$1("scoped-token", ScopedTokenSchema, data);
|
|
6452
6275
|
}
|
|
6453
6276
|
/**
|
|
6454
6277
|
* Manages scoped API tokens with restricted addon/route/capability access.
|
|
@@ -6459,11 +6282,11 @@ var ScopedTokenManager = class {
|
|
|
6459
6282
|
this.store = store;
|
|
6460
6283
|
}
|
|
6461
6284
|
async create(userId, name, scopes, expiresAt) {
|
|
6462
|
-
const rawToken = `${TOKEN_PREFIX}${crypto$
|
|
6463
|
-
const tokenHash = crypto$
|
|
6285
|
+
const rawToken = `${TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("hex")}`;
|
|
6286
|
+
const tokenHash = crypto$1.createHash("sha256").update(rawToken).digest("hex");
|
|
6464
6287
|
const tokenPrefix = rawToken.slice(0, 12);
|
|
6465
6288
|
const record = {
|
|
6466
|
-
id: crypto$
|
|
6289
|
+
id: crypto$1.randomUUID(),
|
|
6467
6290
|
userId,
|
|
6468
6291
|
name,
|
|
6469
6292
|
tokenHash,
|
|
@@ -6487,7 +6310,7 @@ var ScopedTokenManager = class {
|
|
|
6487
6310
|
}
|
|
6488
6311
|
async validate(rawToken) {
|
|
6489
6312
|
if (!rawToken.startsWith(TOKEN_PREFIX)) return null;
|
|
6490
|
-
const tokenHash = crypto$
|
|
6313
|
+
const tokenHash = crypto$1.createHash("sha256").update(rawToken).digest("hex");
|
|
6491
6314
|
const results = await this.store.query.query({
|
|
6492
6315
|
collection: TOKENS_COLLECTION,
|
|
6493
6316
|
filter: { where: { tokenHash } }
|
|
@@ -6610,6 +6433,808 @@ var ScopedTokenManager = class {
|
|
|
6610
6433
|
}
|
|
6611
6434
|
};
|
|
6612
6435
|
//#endregion
|
|
6436
|
+
//#region src/auth/login-session-manager.ts
|
|
6437
|
+
/**
|
|
6438
|
+
* Login sessions — the server-side record behind every session token.
|
|
6439
|
+
*
|
|
6440
|
+
* A session token used to be a self-contained 30-day JWT: nothing on the hub
|
|
6441
|
+
* knew it existed, so logout revoked nothing, a password change left every
|
|
6442
|
+
* other device signed in, and a deleted user's sessions lived until they
|
|
6443
|
+
* expired. Now every login creates a row here and the token carries its id
|
|
6444
|
+
* (`sid`); the hub checks the row on every request (`check`, an in-memory
|
|
6445
|
+
* lookup), so revoking a row ends the session at once.
|
|
6446
|
+
*
|
|
6447
|
+
* Two kinds of client:
|
|
6448
|
+
* - **refreshable** — gets a short access token (default 2 h) and an opaque
|
|
6449
|
+
* refresh token (`csr_…`, stored hashed). `refresh` rotates the refresh
|
|
6450
|
+
* token and slides the session's expiry; presenting a rotated-out refresh
|
|
6451
|
+
* token after a short grace window is treated as theft and revokes the
|
|
6452
|
+
* whole session.
|
|
6453
|
+
* - **legacy** — a client from before refresh tokens: its access token lives
|
|
6454
|
+
* as long as the session, and it renews through the handoff-code pair
|
|
6455
|
+
* (`extend`). Kept for one release so installed viewer apps keep working.
|
|
6456
|
+
*
|
|
6457
|
+
* `legacyCutoff(userId)` covers tokens minted before sessions existed (no
|
|
6458
|
+
* `sid`): a token of that user issued before the cutoff is refused. Revoking
|
|
6459
|
+
* all of a user's sessions moves the cutoff, so a password change or a user
|
|
6460
|
+
* deletion also ends their pre-session tokens.
|
|
6461
|
+
*
|
|
6462
|
+
* Mirror: every row is held in memory (a homelab hub has tens of sessions) and
|
|
6463
|
+
* written through to the store; revocations update the mirror FIRST, so a
|
|
6464
|
+
* store hiccup can delay persistence but never the revocation itself.
|
|
6465
|
+
*/
|
|
6466
|
+
/**
|
|
6467
|
+
* @durable class=ledger owner=local-auth
|
|
6468
|
+
* write="one row per login (password, passkey, SSO, handoff, session exchange);
|
|
6469
|
+
* rewritten on every refresh / renewal (expiry slides, refresh hash rotates)
|
|
6470
|
+
* and on revocation"
|
|
6471
|
+
* retention="the daily reap deletes rows expired more than 7 days ago and rows
|
|
6472
|
+
* revoked more than 30 days ago. A session the user keeps refreshing lives as
|
|
6473
|
+
* long as it is used."
|
|
6474
|
+
*/
|
|
6475
|
+
var LOGIN_SESSIONS_COLLECTION = "login_sessions";
|
|
6476
|
+
/**
|
|
6477
|
+
* @durable class=config owner=local-auth
|
|
6478
|
+
* write="revoking all of a user's sessions (logout everywhere, password change or
|
|
6479
|
+
* reset, user deletion) sets that user's cutoff to now"
|
|
6480
|
+
* retention="none — one row per user who ever had all sessions revoked; it only
|
|
6481
|
+
* ever matters for tokens minted before sessions existed, and is harmless after."
|
|
6482
|
+
*/
|
|
6483
|
+
var LOGIN_SESSION_CUTOFFS_COLLECTION = "login_session_cutoffs";
|
|
6484
|
+
var LOGIN_SESSIONS_COLUMNS = [
|
|
6485
|
+
{
|
|
6486
|
+
name: "id",
|
|
6487
|
+
type: "TEXT",
|
|
6488
|
+
primaryKey: true,
|
|
6489
|
+
notNull: true
|
|
6490
|
+
},
|
|
6491
|
+
{
|
|
6492
|
+
name: "userId",
|
|
6493
|
+
type: "TEXT",
|
|
6494
|
+
notNull: true
|
|
6495
|
+
},
|
|
6496
|
+
{
|
|
6497
|
+
name: "method",
|
|
6498
|
+
type: "TEXT",
|
|
6499
|
+
notNull: true
|
|
6500
|
+
},
|
|
6501
|
+
{
|
|
6502
|
+
name: "refreshable",
|
|
6503
|
+
type: "BOOLEAN",
|
|
6504
|
+
notNull: true
|
|
6505
|
+
},
|
|
6506
|
+
{
|
|
6507
|
+
name: "createdAt",
|
|
6508
|
+
type: "INTEGER",
|
|
6509
|
+
notNull: true
|
|
6510
|
+
},
|
|
6511
|
+
{
|
|
6512
|
+
name: "lastUsedAt",
|
|
6513
|
+
type: "INTEGER",
|
|
6514
|
+
notNull: true
|
|
6515
|
+
},
|
|
6516
|
+
{
|
|
6517
|
+
name: "expiresAt",
|
|
6518
|
+
type: "INTEGER",
|
|
6519
|
+
notNull: true
|
|
6520
|
+
},
|
|
6521
|
+
{
|
|
6522
|
+
name: "revokedAt",
|
|
6523
|
+
type: "INTEGER"
|
|
6524
|
+
},
|
|
6525
|
+
{
|
|
6526
|
+
name: "revokedReason",
|
|
6527
|
+
type: "TEXT"
|
|
6528
|
+
},
|
|
6529
|
+
{
|
|
6530
|
+
name: "refreshHash",
|
|
6531
|
+
type: "TEXT"
|
|
6532
|
+
},
|
|
6533
|
+
{
|
|
6534
|
+
name: "previousRefreshHash",
|
|
6535
|
+
type: "TEXT"
|
|
6536
|
+
},
|
|
6537
|
+
{
|
|
6538
|
+
name: "rotatedAt",
|
|
6539
|
+
type: "INTEGER"
|
|
6540
|
+
},
|
|
6541
|
+
{
|
|
6542
|
+
name: "client",
|
|
6543
|
+
type: "TEXT"
|
|
6544
|
+
},
|
|
6545
|
+
{
|
|
6546
|
+
name: "ip",
|
|
6547
|
+
type: "TEXT"
|
|
6548
|
+
}
|
|
6549
|
+
];
|
|
6550
|
+
var LOGIN_SESSION_CUTOFFS_COLUMNS = [{
|
|
6551
|
+
name: "userId",
|
|
6552
|
+
type: "TEXT",
|
|
6553
|
+
primaryKey: true,
|
|
6554
|
+
notNull: true
|
|
6555
|
+
}, {
|
|
6556
|
+
name: "notBefore",
|
|
6557
|
+
type: "INTEGER",
|
|
6558
|
+
notNull: true
|
|
6559
|
+
}];
|
|
6560
|
+
var METHODS = [
|
|
6561
|
+
"password",
|
|
6562
|
+
"passkey",
|
|
6563
|
+
"sso",
|
|
6564
|
+
"handoff",
|
|
6565
|
+
"exchange"
|
|
6566
|
+
];
|
|
6567
|
+
var REFRESH_TOKEN_PREFIX = "csr_";
|
|
6568
|
+
/**
|
|
6569
|
+
* A rotated-out refresh token is still honoured this long: two tabs (or a
|
|
6570
|
+
* retry after a dropped response) may present the same token a moment apart.
|
|
6571
|
+
* After it, the old token is evidence of a copy and ends the session.
|
|
6572
|
+
*/
|
|
6573
|
+
var REFRESH_REUSE_GRACE_MS = 3e4;
|
|
6574
|
+
var EXPIRED_RETENTION_MS = 7 * 864e5;
|
|
6575
|
+
var REVOKED_RETENTION_MS = 30 * 864e5;
|
|
6576
|
+
/** Longest client label kept (a User-Agent can be huge). */
|
|
6577
|
+
var MAX_CLIENT_LABEL = 200;
|
|
6578
|
+
function hashToken(token) {
|
|
6579
|
+
return crypto$1.createHash("sha256").update(token).digest("hex");
|
|
6580
|
+
}
|
|
6581
|
+
function str(data, key) {
|
|
6582
|
+
const value = data[key];
|
|
6583
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
6584
|
+
}
|
|
6585
|
+
function num(data, key) {
|
|
6586
|
+
const value = data[key];
|
|
6587
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
6588
|
+
}
|
|
6589
|
+
function parseSession$1(data) {
|
|
6590
|
+
const id = str(data, "id");
|
|
6591
|
+
const userId = str(data, "userId");
|
|
6592
|
+
const createdAt = num(data, "createdAt");
|
|
6593
|
+
const lastUsedAt = num(data, "lastUsedAt");
|
|
6594
|
+
const expiresAt = num(data, "expiresAt");
|
|
6595
|
+
const method = METHODS.find((m) => m === data["method"]);
|
|
6596
|
+
if (id === null || userId === null || createdAt === null || expiresAt === null) return null;
|
|
6597
|
+
return {
|
|
6598
|
+
id,
|
|
6599
|
+
userId,
|
|
6600
|
+
method: method ?? "password",
|
|
6601
|
+
refreshable: data["refreshable"] === true || data["refreshable"] === 1,
|
|
6602
|
+
createdAt,
|
|
6603
|
+
lastUsedAt: lastUsedAt ?? createdAt,
|
|
6604
|
+
expiresAt,
|
|
6605
|
+
revokedAt: num(data, "revokedAt"),
|
|
6606
|
+
revokedReason: str(data, "revokedReason"),
|
|
6607
|
+
refreshHash: str(data, "refreshHash"),
|
|
6608
|
+
previousRefreshHash: str(data, "previousRefreshHash"),
|
|
6609
|
+
rotatedAt: num(data, "rotatedAt"),
|
|
6610
|
+
client: str(data, "client"),
|
|
6611
|
+
ip: str(data, "ip")
|
|
6612
|
+
};
|
|
6613
|
+
}
|
|
6614
|
+
function toLoginSessionSummary(record) {
|
|
6615
|
+
const { refreshHash: _r, previousRefreshHash: _p, rotatedAt: _t, ...summary } = record;
|
|
6616
|
+
return summary;
|
|
6617
|
+
}
|
|
6618
|
+
var LoginSessionManager = class {
|
|
6619
|
+
store;
|
|
6620
|
+
sessions = /* @__PURE__ */ new Map();
|
|
6621
|
+
/** refresh hash (current or previous) → session id */
|
|
6622
|
+
byRefreshHash = /* @__PURE__ */ new Map();
|
|
6623
|
+
cutoffs = /* @__PURE__ */ new Map();
|
|
6624
|
+
now;
|
|
6625
|
+
logger;
|
|
6626
|
+
constructor(store, options = {}) {
|
|
6627
|
+
this.store = store;
|
|
6628
|
+
this.now = options.now ?? Date.now;
|
|
6629
|
+
this.logger = options.logger ?? null;
|
|
6630
|
+
}
|
|
6631
|
+
/** Load every row into the mirror. Call once at init, before serving. */
|
|
6632
|
+
async load() {
|
|
6633
|
+
const rows = await this.store.query.query({ collection: LOGIN_SESSIONS_COLLECTION });
|
|
6634
|
+
for (const row of rows) {
|
|
6635
|
+
const record = parseSession$1(row.data);
|
|
6636
|
+
if (record !== null) this.remember(record);
|
|
6637
|
+
}
|
|
6638
|
+
const cutoffs = await this.store.query.query({ collection: LOGIN_SESSION_CUTOFFS_COLLECTION });
|
|
6639
|
+
for (const row of cutoffs) {
|
|
6640
|
+
const userId = str(row.data, "userId");
|
|
6641
|
+
const notBefore = num(row.data, "notBefore");
|
|
6642
|
+
if (userId !== null && notBefore !== null) this.cutoffs.set(userId, notBefore);
|
|
6643
|
+
}
|
|
6644
|
+
}
|
|
6645
|
+
async create(input) {
|
|
6646
|
+
const at = this.now();
|
|
6647
|
+
const refreshToken = input.refreshable ? this.newRefreshToken() : null;
|
|
6648
|
+
const session = {
|
|
6649
|
+
id: crypto$1.randomUUID(),
|
|
6650
|
+
userId: input.userId,
|
|
6651
|
+
method: input.method,
|
|
6652
|
+
refreshable: input.refreshable,
|
|
6653
|
+
createdAt: at,
|
|
6654
|
+
lastUsedAt: at,
|
|
6655
|
+
expiresAt: at + input.lifetimeMs,
|
|
6656
|
+
revokedAt: null,
|
|
6657
|
+
revokedReason: null,
|
|
6658
|
+
refreshHash: refreshToken === null ? null : hashToken(refreshToken),
|
|
6659
|
+
previousRefreshHash: null,
|
|
6660
|
+
rotatedAt: null,
|
|
6661
|
+
client: input.client === void 0 ? null : input.client.slice(0, MAX_CLIENT_LABEL),
|
|
6662
|
+
ip: input.ip ?? null
|
|
6663
|
+
};
|
|
6664
|
+
await this.store.insert.mutate({
|
|
6665
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
6666
|
+
record: {
|
|
6667
|
+
id: session.id,
|
|
6668
|
+
data: { ...session }
|
|
6669
|
+
}
|
|
6670
|
+
});
|
|
6671
|
+
this.remember(session);
|
|
6672
|
+
return {
|
|
6673
|
+
session,
|
|
6674
|
+
refreshToken
|
|
6675
|
+
};
|
|
6676
|
+
}
|
|
6677
|
+
/** Is `sessionId` a live session right now? Synchronous: the hub asks on every request. */
|
|
6678
|
+
check(sessionId) {
|
|
6679
|
+
const session = this.sessions.get(sessionId);
|
|
6680
|
+
if (session === void 0) return { state: "unknown" };
|
|
6681
|
+
if (session.revokedAt !== null) return { state: "revoked" };
|
|
6682
|
+
if (session.expiresAt <= this.now()) return { state: "expired" };
|
|
6683
|
+
return {
|
|
6684
|
+
state: "active",
|
|
6685
|
+
session
|
|
6686
|
+
};
|
|
6687
|
+
}
|
|
6688
|
+
/** Epoch ms before which a pre-session (no `sid`) token of `userId` is refused; 0 = none. */
|
|
6689
|
+
legacyCutoff(userId) {
|
|
6690
|
+
return this.cutoffs.get(userId) ?? 0;
|
|
6691
|
+
}
|
|
6692
|
+
/** Swap a refresh token for a rotated one, sliding the session's expiry to now + `lifetimeMs`. */
|
|
6693
|
+
async refresh(refreshToken, lifetimeMs) {
|
|
6694
|
+
if (!refreshToken.startsWith("csr_")) return {
|
|
6695
|
+
ok: false,
|
|
6696
|
+
reason: "unknown"
|
|
6697
|
+
};
|
|
6698
|
+
const presented = hashToken(refreshToken);
|
|
6699
|
+
const sessionId = this.byRefreshHash.get(presented);
|
|
6700
|
+
const session = sessionId === void 0 ? void 0 : this.sessions.get(sessionId);
|
|
6701
|
+
if (session === void 0) return {
|
|
6702
|
+
ok: false,
|
|
6703
|
+
reason: "unknown"
|
|
6704
|
+
};
|
|
6705
|
+
if (session.revokedAt !== null) return {
|
|
6706
|
+
ok: false,
|
|
6707
|
+
reason: "revoked"
|
|
6708
|
+
};
|
|
6709
|
+
const at = this.now();
|
|
6710
|
+
if (session.expiresAt <= at) return {
|
|
6711
|
+
ok: false,
|
|
6712
|
+
reason: "expired"
|
|
6713
|
+
};
|
|
6714
|
+
if (presented !== session.refreshHash) {
|
|
6715
|
+
if (!(presented === session.previousRefreshHash && session.rotatedAt !== null && at - session.rotatedAt <= REFRESH_REUSE_GRACE_MS)) {
|
|
6716
|
+
await this.revoke(session.id, "refresh-reuse");
|
|
6717
|
+
this.logger?.warn("A rotated-out refresh token was presented — session revoked", { meta: {
|
|
6718
|
+
sessionId: session.id,
|
|
6719
|
+
userId: session.userId
|
|
6720
|
+
} });
|
|
6721
|
+
return {
|
|
6722
|
+
ok: false,
|
|
6723
|
+
reason: "reused"
|
|
6724
|
+
};
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
const next = this.newRefreshToken();
|
|
6728
|
+
const updated = {
|
|
6729
|
+
...session,
|
|
6730
|
+
lastUsedAt: at,
|
|
6731
|
+
expiresAt: at + lifetimeMs,
|
|
6732
|
+
previousRefreshHash: session.refreshHash,
|
|
6733
|
+
refreshHash: hashToken(next),
|
|
6734
|
+
rotatedAt: at
|
|
6735
|
+
};
|
|
6736
|
+
this.replace(session, updated);
|
|
6737
|
+
await this.persist(updated);
|
|
6738
|
+
return {
|
|
6739
|
+
ok: true,
|
|
6740
|
+
session: updated,
|
|
6741
|
+
refreshToken: next
|
|
6742
|
+
};
|
|
6743
|
+
}
|
|
6744
|
+
/** Renew an active session (legacy client): slide its expiry. `null` when it is not active. */
|
|
6745
|
+
async extend(sessionId, lifetimeMs) {
|
|
6746
|
+
const check = this.check(sessionId);
|
|
6747
|
+
if (check.state !== "active") return null;
|
|
6748
|
+
const at = this.now();
|
|
6749
|
+
const updated = {
|
|
6750
|
+
...check.session,
|
|
6751
|
+
lastUsedAt: at,
|
|
6752
|
+
expiresAt: at + lifetimeMs
|
|
6753
|
+
};
|
|
6754
|
+
this.replace(check.session, updated);
|
|
6755
|
+
await this.persist(updated);
|
|
6756
|
+
return updated;
|
|
6757
|
+
}
|
|
6758
|
+
/** End one session. `false` when it was unknown or already ended. */
|
|
6759
|
+
async revoke(sessionId, reason) {
|
|
6760
|
+
const session = this.sessions.get(sessionId);
|
|
6761
|
+
if (session === void 0 || session.revokedAt !== null) return false;
|
|
6762
|
+
const updated = {
|
|
6763
|
+
...session,
|
|
6764
|
+
revokedAt: this.now(),
|
|
6765
|
+
revokedReason: reason
|
|
6766
|
+
};
|
|
6767
|
+
this.replace(session, updated);
|
|
6768
|
+
await this.persist(updated);
|
|
6769
|
+
return true;
|
|
6770
|
+
}
|
|
6771
|
+
/**
|
|
6772
|
+
* End every live session of `userId` except `exceptSessionId`, and move the
|
|
6773
|
+
* user's legacy cutoff to now (pre-session tokens die too). Returns how many
|
|
6774
|
+
* sessions ended.
|
|
6775
|
+
*/
|
|
6776
|
+
async revokeAllForUser(userId, options) {
|
|
6777
|
+
const at = this.now();
|
|
6778
|
+
this.cutoffs.set(userId, at);
|
|
6779
|
+
const ended = [];
|
|
6780
|
+
for (const session of this.sessions.values()) {
|
|
6781
|
+
if (session.userId !== userId || session.revokedAt !== null) continue;
|
|
6782
|
+
if (session.id === options.exceptSessionId) continue;
|
|
6783
|
+
const updated = {
|
|
6784
|
+
...session,
|
|
6785
|
+
revokedAt: at,
|
|
6786
|
+
revokedReason: options.reason
|
|
6787
|
+
};
|
|
6788
|
+
this.replace(session, updated);
|
|
6789
|
+
ended.push(updated);
|
|
6790
|
+
}
|
|
6791
|
+
await this.persistCutoff(userId, at);
|
|
6792
|
+
for (const session of ended) await this.persist(session);
|
|
6793
|
+
return ended.length;
|
|
6794
|
+
}
|
|
6795
|
+
/** `userId`'s live sessions, newest first. */
|
|
6796
|
+
listForUser(userId) {
|
|
6797
|
+
return this.listActive().filter((s) => s.userId === userId);
|
|
6798
|
+
}
|
|
6799
|
+
/** Every live session, newest first. */
|
|
6800
|
+
listActive() {
|
|
6801
|
+
const at = this.now();
|
|
6802
|
+
return [...this.sessions.values()].filter((s) => s.revokedAt === null && s.expiresAt > at).toSorted((a, b) => b.createdAt - a.createdAt).map(toLoginSessionSummary);
|
|
6803
|
+
}
|
|
6804
|
+
/** Delete rows expired more than 7 days ago or revoked more than 30. Returns how many. */
|
|
6805
|
+
async reap() {
|
|
6806
|
+
const at = this.now();
|
|
6807
|
+
const dead = [...this.sessions.values()].filter((s) => s.revokedAt !== null && at - s.revokedAt > REVOKED_RETENTION_MS || s.revokedAt === null && at - s.expiresAt > EXPIRED_RETENTION_MS);
|
|
6808
|
+
for (const session of dead) {
|
|
6809
|
+
this.forget(session);
|
|
6810
|
+
await this.store.delete.mutate({
|
|
6811
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
6812
|
+
key: session.id
|
|
6813
|
+
});
|
|
6814
|
+
}
|
|
6815
|
+
return dead.length;
|
|
6816
|
+
}
|
|
6817
|
+
newRefreshToken() {
|
|
6818
|
+
return `${REFRESH_TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("base64url")}`;
|
|
6819
|
+
}
|
|
6820
|
+
remember(session) {
|
|
6821
|
+
this.sessions.set(session.id, session);
|
|
6822
|
+
if (session.refreshHash !== null) this.byRefreshHash.set(session.refreshHash, session.id);
|
|
6823
|
+
if (session.previousRefreshHash !== null) this.byRefreshHash.set(session.previousRefreshHash, session.id);
|
|
6824
|
+
}
|
|
6825
|
+
forget(session) {
|
|
6826
|
+
this.sessions.delete(session.id);
|
|
6827
|
+
if (session.refreshHash !== null) this.byRefreshHash.delete(session.refreshHash);
|
|
6828
|
+
if (session.previousRefreshHash !== null) this.byRefreshHash.delete(session.previousRefreshHash);
|
|
6829
|
+
}
|
|
6830
|
+
replace(previous, next) {
|
|
6831
|
+
this.forget(previous);
|
|
6832
|
+
this.remember(next);
|
|
6833
|
+
}
|
|
6834
|
+
async persist(session) {
|
|
6835
|
+
try {
|
|
6836
|
+
await this.store.update.mutate({
|
|
6837
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
6838
|
+
id: session.id,
|
|
6839
|
+
data: { ...session }
|
|
6840
|
+
});
|
|
6841
|
+
} catch (error) {
|
|
6842
|
+
this.logger?.error("Login session write failed — the change holds until restart", { meta: {
|
|
6843
|
+
sessionId: session.id,
|
|
6844
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6845
|
+
} });
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
async persistCutoff(userId, notBefore) {
|
|
6849
|
+
try {
|
|
6850
|
+
await this.store.delete.mutate({
|
|
6851
|
+
collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
|
|
6852
|
+
key: userId
|
|
6853
|
+
});
|
|
6854
|
+
await this.store.insert.mutate({
|
|
6855
|
+
collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
|
|
6856
|
+
record: {
|
|
6857
|
+
id: userId,
|
|
6858
|
+
data: {
|
|
6859
|
+
userId,
|
|
6860
|
+
notBefore
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
});
|
|
6864
|
+
} catch (error) {
|
|
6865
|
+
this.logger?.error("Login session cutoff write failed — it holds until restart", { meta: {
|
|
6866
|
+
userId,
|
|
6867
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6868
|
+
} });
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
};
|
|
6872
|
+
//#endregion
|
|
6873
|
+
//#region src/auth/admin-token-manager.ts
|
|
6874
|
+
/**
|
|
6875
|
+
* Admin tokens (`csa_…`) — the one credential an ops script can hold that
|
|
6876
|
+
* acts as an administrator, minted deliberately and kept on a short leash.
|
|
6877
|
+
*
|
|
6878
|
+
* Scoped tokens (`cst_`) are never admin, and a script that needs admin (the
|
|
6879
|
+
* keychain migration, a release's `applyServerUpdate`) otherwise needs a human
|
|
6880
|
+
* login. This is that credential, with the rules the operator set:
|
|
6881
|
+
*
|
|
6882
|
+
* - created only by an admin, from the UI, after re-authentication (the hub
|
|
6883
|
+
* router enforces both);
|
|
6884
|
+
* - expiry MANDATORY, at most 90 days;
|
|
6885
|
+
* - shown once — only a SHA-256 hash is stored, like `cst_`;
|
|
6886
|
+
* - every use audit-logged (the hub, per request);
|
|
6887
|
+
* - revocable, and dead the moment its creator is deleted or loses admin;
|
|
6888
|
+
* - NOT a session: an opaque token, so it survives a signing-key change.
|
|
6889
|
+
*/
|
|
6890
|
+
/**
|
|
6891
|
+
* @durable class=config owner=local-auth
|
|
6892
|
+
* write="an admin creates a token (after re-authentication); `lastUsedAt` is
|
|
6893
|
+
* rewritten at most once a minute while it is used; `revoke` stamps revokedAt"
|
|
6894
|
+
* retention="the daily reap deletes rows expired or revoked more than 30 days ago"
|
|
6895
|
+
*/
|
|
6896
|
+
var ADMIN_TOKENS_COLLECTION = "admin_tokens";
|
|
6897
|
+
var ADMIN_TOKENS_COLUMNS = [
|
|
6898
|
+
{
|
|
6899
|
+
name: "id",
|
|
6900
|
+
type: "TEXT",
|
|
6901
|
+
primaryKey: true,
|
|
6902
|
+
notNull: true
|
|
6903
|
+
},
|
|
6904
|
+
{
|
|
6905
|
+
name: "createdBy",
|
|
6906
|
+
type: "TEXT",
|
|
6907
|
+
notNull: true
|
|
6908
|
+
},
|
|
6909
|
+
{
|
|
6910
|
+
name: "name",
|
|
6911
|
+
type: "TEXT",
|
|
6912
|
+
notNull: true
|
|
6913
|
+
},
|
|
6914
|
+
{
|
|
6915
|
+
name: "tokenHash",
|
|
6916
|
+
type: "TEXT",
|
|
6917
|
+
notNull: true,
|
|
6918
|
+
unique: true
|
|
6919
|
+
},
|
|
6920
|
+
{
|
|
6921
|
+
name: "tokenPrefix",
|
|
6922
|
+
type: "TEXT",
|
|
6923
|
+
notNull: true
|
|
6924
|
+
},
|
|
6925
|
+
{
|
|
6926
|
+
name: "createdAt",
|
|
6927
|
+
type: "INTEGER",
|
|
6928
|
+
notNull: true
|
|
6929
|
+
},
|
|
6930
|
+
{
|
|
6931
|
+
name: "expiresAt",
|
|
6932
|
+
type: "INTEGER",
|
|
6933
|
+
notNull: true
|
|
6934
|
+
},
|
|
6935
|
+
{
|
|
6936
|
+
name: "lastUsedAt",
|
|
6937
|
+
type: "INTEGER"
|
|
6938
|
+
},
|
|
6939
|
+
{
|
|
6940
|
+
name: "revokedAt",
|
|
6941
|
+
type: "INTEGER"
|
|
6942
|
+
}
|
|
6943
|
+
];
|
|
6944
|
+
var ADMIN_TOKEN_PREFIX = "csa_";
|
|
6945
|
+
var ADMIN_TOKEN_MAX_LIFETIME_MS = 90 * 864e5;
|
|
6946
|
+
var DEAD_RETENTION_MS = 30 * 864e5;
|
|
6947
|
+
var TOUCH_EVERY_MS = 6e4;
|
|
6948
|
+
function hash(token) {
|
|
6949
|
+
return crypto$1.createHash("sha256").update(token).digest("hex");
|
|
6950
|
+
}
|
|
6951
|
+
function parse(data) {
|
|
6952
|
+
const { id, createdBy, name, tokenHash, tokenPrefix, createdAt, expiresAt } = data;
|
|
6953
|
+
if (typeof id !== "string" || typeof createdBy !== "string" || typeof name !== "string" || typeof tokenHash !== "string" || typeof tokenPrefix !== "string" || typeof createdAt !== "number" || typeof expiresAt !== "number") return null;
|
|
6954
|
+
return {
|
|
6955
|
+
id,
|
|
6956
|
+
createdBy,
|
|
6957
|
+
name,
|
|
6958
|
+
tokenHash,
|
|
6959
|
+
tokenPrefix,
|
|
6960
|
+
createdAt,
|
|
6961
|
+
expiresAt,
|
|
6962
|
+
lastUsedAt: typeof data["lastUsedAt"] === "number" ? data["lastUsedAt"] : null,
|
|
6963
|
+
revokedAt: typeof data["revokedAt"] === "number" ? data["revokedAt"] : null
|
|
6964
|
+
};
|
|
6965
|
+
}
|
|
6966
|
+
function summary(stored) {
|
|
6967
|
+
const { tokenHash: _hash, ...record } = stored;
|
|
6968
|
+
return record;
|
|
6969
|
+
}
|
|
6970
|
+
var AdminTokenManager = class {
|
|
6971
|
+
store;
|
|
6972
|
+
now;
|
|
6973
|
+
constructor(store, now = Date.now) {
|
|
6974
|
+
this.store = store;
|
|
6975
|
+
this.now = now;
|
|
6976
|
+
}
|
|
6977
|
+
/** Mint a token for `createdBy`. Refuses an expiry in the past or past 90 days. */
|
|
6978
|
+
async create(input) {
|
|
6979
|
+
const at = this.now();
|
|
6980
|
+
if (!(input.expiresAt > at) || input.expiresAt > at + 7776e6) throw new Error("An admin token must expire within 90 days");
|
|
6981
|
+
const name = input.name.trim().slice(0, 80);
|
|
6982
|
+
if (name.length === 0) throw new Error("An admin token needs a name");
|
|
6983
|
+
const token = `${ADMIN_TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("base64url")}`;
|
|
6984
|
+
const stored = {
|
|
6985
|
+
id: crypto$1.randomUUID(),
|
|
6986
|
+
createdBy: input.createdBy,
|
|
6987
|
+
name,
|
|
6988
|
+
tokenHash: hash(token),
|
|
6989
|
+
tokenPrefix: token.slice(0, 12),
|
|
6990
|
+
createdAt: at,
|
|
6991
|
+
expiresAt: input.expiresAt,
|
|
6992
|
+
lastUsedAt: null,
|
|
6993
|
+
revokedAt: null
|
|
6994
|
+
};
|
|
6995
|
+
await this.store.insert.mutate({
|
|
6996
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
6997
|
+
record: {
|
|
6998
|
+
id: stored.id,
|
|
6999
|
+
data: { ...stored }
|
|
7000
|
+
}
|
|
7001
|
+
});
|
|
7002
|
+
return {
|
|
7003
|
+
token,
|
|
7004
|
+
record: summary(stored)
|
|
7005
|
+
};
|
|
7006
|
+
}
|
|
7007
|
+
/** The live record behind `token`, or `null` (unknown, revoked, expired, not a `csa_`). */
|
|
7008
|
+
async validate(token) {
|
|
7009
|
+
if (!token.startsWith("csa_")) return null;
|
|
7010
|
+
const first = (await this.store.query.query({
|
|
7011
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7012
|
+
filter: { where: { tokenHash: hash(token) } }
|
|
7013
|
+
}))[0];
|
|
7014
|
+
const stored = first === void 0 ? null : parse(first.data);
|
|
7015
|
+
if (stored === null || stored.revokedAt !== null) return null;
|
|
7016
|
+
const at = this.now();
|
|
7017
|
+
if (stored.expiresAt <= at) return null;
|
|
7018
|
+
if (stored.lastUsedAt === null || at - stored.lastUsedAt > TOUCH_EVERY_MS) this.store.update.mutate({
|
|
7019
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7020
|
+
id: stored.id,
|
|
7021
|
+
data: {
|
|
7022
|
+
...stored,
|
|
7023
|
+
lastUsedAt: at
|
|
7024
|
+
}
|
|
7025
|
+
}).catch(() => {});
|
|
7026
|
+
return summary(stored);
|
|
7027
|
+
}
|
|
7028
|
+
/** Every token not yet reaped, newest first (no hashes). */
|
|
7029
|
+
async list() {
|
|
7030
|
+
return (await this.store.query.query({ collection: ADMIN_TOKENS_COLLECTION })).map((row) => parse(row.data)).filter((r) => r !== null).map(summary).toSorted((a, b) => b.createdAt - a.createdAt);
|
|
7031
|
+
}
|
|
7032
|
+
/** Revoke one token. `false` when unknown or already revoked. */
|
|
7033
|
+
async revoke(id) {
|
|
7034
|
+
const first = (await this.store.query.query({
|
|
7035
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7036
|
+
filter: { where: { id } }
|
|
7037
|
+
}))[0];
|
|
7038
|
+
const stored = first === void 0 ? null : parse(first.data);
|
|
7039
|
+
if (stored === null || stored.revokedAt !== null) return false;
|
|
7040
|
+
await this.store.update.mutate({
|
|
7041
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7042
|
+
id,
|
|
7043
|
+
data: {
|
|
7044
|
+
...stored,
|
|
7045
|
+
revokedAt: this.now()
|
|
7046
|
+
}
|
|
7047
|
+
});
|
|
7048
|
+
return true;
|
|
7049
|
+
}
|
|
7050
|
+
/** Revoke every live token `userId` created (the user was deleted). */
|
|
7051
|
+
async revokeAllCreatedBy(userId) {
|
|
7052
|
+
let revoked = 0;
|
|
7053
|
+
for (const token of await this.list()) if (token.createdBy === userId && token.revokedAt === null) {
|
|
7054
|
+
if (await this.revoke(token.id)) revoked++;
|
|
7055
|
+
}
|
|
7056
|
+
return revoked;
|
|
7057
|
+
}
|
|
7058
|
+
/** Delete rows expired or revoked more than 30 days ago. */
|
|
7059
|
+
async reap() {
|
|
7060
|
+
const horizon = this.now() - DEAD_RETENTION_MS;
|
|
7061
|
+
let deleted = 0;
|
|
7062
|
+
for (const token of await this.list()) if ((token.revokedAt ?? token.expiresAt) < horizon) {
|
|
7063
|
+
await this.store.delete.mutate({
|
|
7064
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7065
|
+
key: token.id
|
|
7066
|
+
});
|
|
7067
|
+
deleted++;
|
|
7068
|
+
}
|
|
7069
|
+
return deleted;
|
|
7070
|
+
}
|
|
7071
|
+
};
|
|
7072
|
+
//#endregion
|
|
7073
|
+
//#region src/auth/auth-audit-log.ts
|
|
7074
|
+
/**
|
|
7075
|
+
* The authentication audit trail: what an operator must be able to answer
|
|
7076
|
+
* after the fact — "who skipped 2FA from the LAN, and from where?", "which
|
|
7077
|
+
* admin token ran this, and when?".
|
|
7078
|
+
*
|
|
7079
|
+
* One row per event, kept 90 days. Writing never throws: an audit row that
|
|
7080
|
+
* cannot be stored is logged loudly instead, and never blocks the action
|
|
7081
|
+
* (a login must not fail because the audit table is busy).
|
|
7082
|
+
*/
|
|
7083
|
+
/**
|
|
7084
|
+
* @durable class=ledger owner=local-auth
|
|
7085
|
+
* write="one row per audited auth event: a second factor skipped from a trusted
|
|
7086
|
+
* network, an admin token created / used / revoked"
|
|
7087
|
+
* retention="the daily reap deletes rows older than 90 days"
|
|
7088
|
+
*/
|
|
7089
|
+
var AUTH_AUDIT_COLLECTION = "auth_audit";
|
|
7090
|
+
var AUTH_AUDIT_COLUMNS = [
|
|
7091
|
+
{
|
|
7092
|
+
name: "id",
|
|
7093
|
+
type: "TEXT",
|
|
7094
|
+
primaryKey: true,
|
|
7095
|
+
notNull: true
|
|
7096
|
+
},
|
|
7097
|
+
{
|
|
7098
|
+
name: "at",
|
|
7099
|
+
type: "INTEGER",
|
|
7100
|
+
notNull: true
|
|
7101
|
+
},
|
|
7102
|
+
{
|
|
7103
|
+
name: "action",
|
|
7104
|
+
type: "TEXT",
|
|
7105
|
+
notNull: true
|
|
7106
|
+
},
|
|
7107
|
+
{
|
|
7108
|
+
name: "userId",
|
|
7109
|
+
type: "TEXT"
|
|
7110
|
+
},
|
|
7111
|
+
{
|
|
7112
|
+
name: "ip",
|
|
7113
|
+
type: "TEXT"
|
|
7114
|
+
},
|
|
7115
|
+
{
|
|
7116
|
+
name: "detail",
|
|
7117
|
+
type: "TEXT"
|
|
7118
|
+
}
|
|
7119
|
+
];
|
|
7120
|
+
var AUTH_AUDIT_ACTIONS = [
|
|
7121
|
+
"second-factor-skipped",
|
|
7122
|
+
"admin-token-created",
|
|
7123
|
+
"admin-token-used",
|
|
7124
|
+
"admin-token-revoked"
|
|
7125
|
+
];
|
|
7126
|
+
var RETENTION_MS = 90 * 864e5;
|
|
7127
|
+
var MAX_LIST = 500;
|
|
7128
|
+
function parseRecord(data) {
|
|
7129
|
+
const id = data["id"];
|
|
7130
|
+
const at = data["at"];
|
|
7131
|
+
const action = data["action"];
|
|
7132
|
+
const known = AUTH_AUDIT_ACTIONS.find((a) => a === action);
|
|
7133
|
+
if (typeof id !== "string" || typeof at !== "number" || known === void 0) return null;
|
|
7134
|
+
let detail;
|
|
7135
|
+
if (typeof data["detail"] === "string") try {
|
|
7136
|
+
const parsed = JSON.parse(data["detail"]);
|
|
7137
|
+
if (parsed !== null && typeof parsed === "object") detail = Object.fromEntries(Object.entries(parsed).filter((e) => typeof e[1] === "string" || typeof e[1] === "number" || typeof e[1] === "boolean"));
|
|
7138
|
+
} catch {
|
|
7139
|
+
detail = void 0;
|
|
7140
|
+
}
|
|
7141
|
+
return {
|
|
7142
|
+
id,
|
|
7143
|
+
at,
|
|
7144
|
+
action: known,
|
|
7145
|
+
...typeof data["userId"] === "string" ? { userId: data["userId"] } : {},
|
|
7146
|
+
...typeof data["ip"] === "string" ? { ip: data["ip"] } : {},
|
|
7147
|
+
...detail !== void 0 ? { detail } : {}
|
|
7148
|
+
};
|
|
7149
|
+
}
|
|
7150
|
+
var AuthAuditLog = class {
|
|
7151
|
+
store;
|
|
7152
|
+
logger;
|
|
7153
|
+
now;
|
|
7154
|
+
constructor(store, logger = null, now = Date.now) {
|
|
7155
|
+
this.store = store;
|
|
7156
|
+
this.logger = logger;
|
|
7157
|
+
this.now = now;
|
|
7158
|
+
}
|
|
7159
|
+
/** Record `event`. Never throws. */
|
|
7160
|
+
async record(event) {
|
|
7161
|
+
const row = {
|
|
7162
|
+
id: crypto$1.randomUUID(),
|
|
7163
|
+
at: this.now(),
|
|
7164
|
+
action: event.action,
|
|
7165
|
+
userId: event.userId ?? null,
|
|
7166
|
+
ip: event.ip ?? null,
|
|
7167
|
+
detail: event.detail === void 0 ? null : JSON.stringify(event.detail)
|
|
7168
|
+
};
|
|
7169
|
+
this.logger?.info(`auth audit: ${event.action}`, { meta: {
|
|
7170
|
+
userId: event.userId,
|
|
7171
|
+
ip: event.ip,
|
|
7172
|
+
...event.detail
|
|
7173
|
+
} });
|
|
7174
|
+
try {
|
|
7175
|
+
await this.store.insert.mutate({
|
|
7176
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7177
|
+
record: {
|
|
7178
|
+
id: row.id,
|
|
7179
|
+
data: row
|
|
7180
|
+
}
|
|
7181
|
+
});
|
|
7182
|
+
} catch (error) {
|
|
7183
|
+
this.logger?.error("auth audit row not written", { meta: {
|
|
7184
|
+
action: event.action,
|
|
7185
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7186
|
+
} });
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
/** Newest first, at most `limit` (≤ 500), optionally one user's. */
|
|
7190
|
+
async list(input = {}) {
|
|
7191
|
+
const rows = await this.store.query.query({
|
|
7192
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7193
|
+
...input.userId !== void 0 ? { filter: { where: { userId: input.userId } } } : {}
|
|
7194
|
+
});
|
|
7195
|
+
const limit = Math.min(MAX_LIST, Math.max(1, input.limit ?? 100));
|
|
7196
|
+
return rows.map((row) => parseRecord(row.data)).filter((r) => r !== null).toSorted((a, b) => b.at - a.at).slice(0, limit);
|
|
7197
|
+
}
|
|
7198
|
+
/** Delete rows older than 90 days. Returns how many. */
|
|
7199
|
+
async reap() {
|
|
7200
|
+
const horizon = this.now() - RETENTION_MS;
|
|
7201
|
+
const rows = await this.store.query.query({ collection: AUTH_AUDIT_COLLECTION });
|
|
7202
|
+
let deleted = 0;
|
|
7203
|
+
for (const row of rows) {
|
|
7204
|
+
const at = row.data["at"];
|
|
7205
|
+
if (typeof at === "number" && at < horizon) {
|
|
7206
|
+
await this.store.delete.mutate({
|
|
7207
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7208
|
+
key: String(row.id)
|
|
7209
|
+
});
|
|
7210
|
+
deleted++;
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
return deleted;
|
|
7214
|
+
}
|
|
7215
|
+
};
|
|
7216
|
+
//#endregion
|
|
7217
|
+
//#region src/auth/user-management-internal.ts
|
|
7218
|
+
/** Process-global, so every bundled copy of this module agrees on the key. */
|
|
7219
|
+
var USER_MANAGEMENT_INTERNAL = Symbol.for("camstack.user-management.internal");
|
|
7220
|
+
/** Attach `internal` to `provider` (returns the same object, now carrying it). */
|
|
7221
|
+
function attachUserManagementInternal(provider, internal) {
|
|
7222
|
+
Object.defineProperty(provider, USER_MANAGEMENT_INTERNAL, {
|
|
7223
|
+
value: internal,
|
|
7224
|
+
enumerable: false,
|
|
7225
|
+
writable: false,
|
|
7226
|
+
configurable: false
|
|
7227
|
+
});
|
|
7228
|
+
return provider;
|
|
7229
|
+
}
|
|
7230
|
+
/** The hub-internal surface of a `user-management` provider, or `null` when it carries none. */
|
|
7231
|
+
function getUserManagementInternal(provider) {
|
|
7232
|
+
if (provider === null || typeof provider !== "object") return null;
|
|
7233
|
+
const internal = Reflect.get(provider, USER_MANAGEMENT_INTERNAL);
|
|
7234
|
+
if (internal === null || typeof internal !== "object") return null;
|
|
7235
|
+
return internal;
|
|
7236
|
+
}
|
|
7237
|
+
//#endregion
|
|
6613
7238
|
//#region src/auth/user-manager.ts
|
|
6614
7239
|
var USERS_COLLECTION$1 = "users";
|
|
6615
7240
|
/**
|
|
@@ -6621,7 +7246,7 @@ var USERS_COLLECTION$1 = "users";
|
|
|
6621
7246
|
var FIRST_BOOT_ADMIN_USERNAME = "admin";
|
|
6622
7247
|
var FIRST_BOOT_ADMIN_PASSWORD = "changeme";
|
|
6623
7248
|
function parseUser(data) {
|
|
6624
|
-
return parseRecord("user", UserRecordSchema, data);
|
|
7249
|
+
return parseRecord$1("user", UserRecordSchema, data);
|
|
6625
7250
|
}
|
|
6626
7251
|
var UserManager = class {
|
|
6627
7252
|
storageAccess;
|
|
@@ -6638,7 +7263,7 @@ var UserManager = class {
|
|
|
6638
7263
|
const passwordHash = await this.auth.hashPassword(input.password);
|
|
6639
7264
|
const now = Date.now();
|
|
6640
7265
|
const record = {
|
|
6641
|
-
id: crypto$
|
|
7266
|
+
id: crypto$1.randomUUID(),
|
|
6642
7267
|
username: input.username,
|
|
6643
7268
|
passwordHash,
|
|
6644
7269
|
isAdmin: input.isAdmin ?? false,
|
|
@@ -7195,6 +7820,34 @@ async function declareAuthSchema(store) {
|
|
|
7195
7820
|
columns: ["userId"]
|
|
7196
7821
|
}]
|
|
7197
7822
|
});
|
|
7823
|
+
await store.declareCollection.mutate({
|
|
7824
|
+
collection: LOGIN_SESSIONS_COLLECTION,
|
|
7825
|
+
columns: LOGIN_SESSIONS_COLUMNS,
|
|
7826
|
+
indexes: [{
|
|
7827
|
+
name: "idx_login_sessions_user_id",
|
|
7828
|
+
columns: ["userId"]
|
|
7829
|
+
}]
|
|
7830
|
+
});
|
|
7831
|
+
await store.declareCollection.mutate({
|
|
7832
|
+
collection: LOGIN_SESSION_CUTOFFS_COLLECTION,
|
|
7833
|
+
columns: LOGIN_SESSION_CUTOFFS_COLUMNS
|
|
7834
|
+
});
|
|
7835
|
+
await store.declareCollection.mutate({
|
|
7836
|
+
collection: ADMIN_TOKENS_COLLECTION,
|
|
7837
|
+
columns: ADMIN_TOKENS_COLUMNS,
|
|
7838
|
+
indexes: [{
|
|
7839
|
+
name: "idx_admin_tokens_created_by",
|
|
7840
|
+
columns: ["createdBy"]
|
|
7841
|
+
}]
|
|
7842
|
+
});
|
|
7843
|
+
await store.declareCollection.mutate({
|
|
7844
|
+
collection: AUTH_AUDIT_COLLECTION,
|
|
7845
|
+
columns: AUTH_AUDIT_COLUMNS,
|
|
7846
|
+
indexes: [{
|
|
7847
|
+
name: "idx_auth_audit_user_id",
|
|
7848
|
+
columns: ["userId"]
|
|
7849
|
+
}]
|
|
7850
|
+
});
|
|
7198
7851
|
}
|
|
7199
7852
|
//#endregion
|
|
7200
7853
|
//#region src/auth/external-identity-manager.ts
|
|
@@ -9915,7 +10568,7 @@ var OauthSessionManager = class {
|
|
|
9915
10568
|
async create(input) {
|
|
9916
10569
|
const now = Date.now();
|
|
9917
10570
|
const session = {
|
|
9918
|
-
id: crypto$
|
|
10571
|
+
id: crypto$1.randomUUID(),
|
|
9919
10572
|
userId: input.userId,
|
|
9920
10573
|
username: input.username,
|
|
9921
10574
|
integrationId: input.integrationId,
|
|
@@ -10068,6 +10721,9 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10068
10721
|
totpManager = null;
|
|
10069
10722
|
externalIdentities = null;
|
|
10070
10723
|
signingKeyKeeper = null;
|
|
10724
|
+
loginSessions = null;
|
|
10725
|
+
adminTokens = null;
|
|
10726
|
+
auditLog = null;
|
|
10071
10727
|
constructor() {
|
|
10072
10728
|
super({});
|
|
10073
10729
|
}
|
|
@@ -10088,11 +10744,15 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10088
10744
|
return store;
|
|
10089
10745
|
} };
|
|
10090
10746
|
this.userManager = new UserManager(storageAccess, this.authManager);
|
|
10091
|
-
this.apiKeyManager = new ApiKeyManager(storageAccess
|
|
10747
|
+
this.apiKeyManager = new ApiKeyManager(storageAccess);
|
|
10092
10748
|
this.scopedTokenManager = new ScopedTokenManager(store);
|
|
10093
10749
|
this.oauthSessionManager = new OauthSessionManager(store);
|
|
10094
10750
|
this.totpManager = new TotpManager(store);
|
|
10095
10751
|
this.externalIdentities = new ExternalIdentityManager(store);
|
|
10752
|
+
this.loginSessions = new LoginSessionManager(store, { logger: this.ctx.logger });
|
|
10753
|
+
await this.loginSessions.load();
|
|
10754
|
+
this.adminTokens = new AdminTokenManager(store);
|
|
10755
|
+
this.auditLog = new AuthAuditLog(store, this.ctx.logger);
|
|
10096
10756
|
try {
|
|
10097
10757
|
await reconcileAdminAccountAtBoot({
|
|
10098
10758
|
users: this.userManager,
|
|
@@ -10185,12 +10845,13 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10185
10845
|
deleteUser: async (input) => {
|
|
10186
10846
|
if (!this.userManager) throw new Error("User management not available");
|
|
10187
10847
|
await this.userManager.delete(input.id);
|
|
10188
|
-
await this.
|
|
10848
|
+
await this.endAllAccess(input.id);
|
|
10189
10849
|
return { success: true };
|
|
10190
10850
|
},
|
|
10191
10851
|
resetPassword: async (input) => {
|
|
10192
10852
|
if (!this.userManager) throw new Error("User management not available");
|
|
10193
10853
|
await this.userManager.resetPassword(input.id, input.newPassword);
|
|
10854
|
+
await this.loginSessions?.revokeAllForUser(input.id, { reason: "password-reset" });
|
|
10194
10855
|
return { success: true };
|
|
10195
10856
|
},
|
|
10196
10857
|
setUserScopes: async (input) => {
|
|
@@ -10212,27 +10873,11 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10212
10873
|
if (!this.apiKeyManager) return [];
|
|
10213
10874
|
return this.apiKeyManager.listAll();
|
|
10214
10875
|
},
|
|
10215
|
-
createApiKey: async (input) => {
|
|
10216
|
-
if (!this.apiKeyManager) throw new Error("API key management not available");
|
|
10217
|
-
const { record, token } = await this.apiKeyManager.create(input);
|
|
10218
|
-
const { tokenHash: _, ...summary } = record;
|
|
10219
|
-
return {
|
|
10220
|
-
token,
|
|
10221
|
-
record: summary
|
|
10222
|
-
};
|
|
10223
|
-
},
|
|
10224
10876
|
revokeApiKey: async (input) => {
|
|
10225
10877
|
if (!this.apiKeyManager) throw new Error("API key management not available");
|
|
10226
10878
|
await this.apiKeyManager.revoke(input.id);
|
|
10227
10879
|
return { success: true };
|
|
10228
10880
|
},
|
|
10229
|
-
validateApiKey: async (input) => {
|
|
10230
|
-
if (!this.apiKeyManager) return null;
|
|
10231
|
-
const record = await this.apiKeyManager.validateToken(input.token);
|
|
10232
|
-
if (!record) return null;
|
|
10233
|
-
const { tokenHash: _, ...summary } = record;
|
|
10234
|
-
return summary;
|
|
10235
|
-
},
|
|
10236
10881
|
createScopedToken: async (input) => {
|
|
10237
10882
|
if (!this.scopedTokenManager) throw new Error("Scoped token management not available");
|
|
10238
10883
|
const { token, record } = await this.scopedTokenManager.create(input.userId, input.name, input.scopes, input.expiresAt);
|
|
@@ -10247,8 +10892,10 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10247
10892
|
return { success: true };
|
|
10248
10893
|
},
|
|
10249
10894
|
validateScopedToken: async (input) => {
|
|
10250
|
-
if (!this.scopedTokenManager) return null;
|
|
10251
|
-
|
|
10895
|
+
if (!this.scopedTokenManager || !this.userManager) return null;
|
|
10896
|
+
const record = await this.scopedTokenManager.validate(input.token);
|
|
10897
|
+
if (record === null) return null;
|
|
10898
|
+
return await this.userManager.findById(record.userId) === null ? null : record;
|
|
10252
10899
|
},
|
|
10253
10900
|
listScopedTokens: async (input) => {
|
|
10254
10901
|
if (!this.scopedTokenManager) return [];
|
|
@@ -10291,12 +10938,6 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10291
10938
|
issuer: link.issuer
|
|
10292
10939
|
};
|
|
10293
10940
|
},
|
|
10294
|
-
linkExternalIdentity: async (input) => {
|
|
10295
|
-
if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
|
|
10296
|
-
if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
|
|
10297
|
-
await this.externalIdentities.link(input);
|
|
10298
|
-
return { success: true };
|
|
10299
|
-
},
|
|
10300
10941
|
unlinkExternalIdentity: async (input) => {
|
|
10301
10942
|
if (!this.externalIdentities) return { success: false };
|
|
10302
10943
|
return { success: await this.externalIdentities.unlink(input) };
|
|
@@ -10334,6 +10975,7 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10334
10975
|
}
|
|
10335
10976
|
};
|
|
10336
10977
|
this.ctx.logger.info("registered auth-provider + user-management capabilities");
|
|
10978
|
+
attachUserManagementInternal(userMgmt, this.buildInternal());
|
|
10337
10979
|
return [{
|
|
10338
10980
|
capability: authProviderCapability,
|
|
10339
10981
|
provider: authProvider
|
|
@@ -10343,6 +10985,93 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10343
10985
|
}];
|
|
10344
10986
|
}
|
|
10345
10987
|
/**
|
|
10988
|
+
* The hub-internal surface (`user-management-internal.ts`): operations no
|
|
10989
|
+
* wire may reach. Called only by hub code holding this provider object.
|
|
10990
|
+
*/
|
|
10991
|
+
buildInternal() {
|
|
10992
|
+
const sessions = () => {
|
|
10993
|
+
if (this.loginSessions === null) throw new Error("Login sessions not available");
|
|
10994
|
+
return this.loginSessions;
|
|
10995
|
+
};
|
|
10996
|
+
return {
|
|
10997
|
+
sessions: {
|
|
10998
|
+
create: (input) => sessions().create(input),
|
|
10999
|
+
refresh: (token, lifetimeMs) => sessions().refresh(token, lifetimeMs),
|
|
11000
|
+
extend: (id, lifetimeMs) => sessions().extend(id, lifetimeMs),
|
|
11001
|
+
check: (id) => sessions().check(id),
|
|
11002
|
+
legacyCutoff: (userId) => sessions().legacyCutoff(userId),
|
|
11003
|
+
revoke: (id, reason) => sessions().revoke(id, reason),
|
|
11004
|
+
revokeAllForUser: (userId, options) => sessions().revokeAllForUser(userId, options),
|
|
11005
|
+
listForUser: (userId) => sessions().listForUser(userId),
|
|
11006
|
+
listActive: () => sessions().listActive()
|
|
11007
|
+
},
|
|
11008
|
+
adminTokens: {
|
|
11009
|
+
create: async (input) => {
|
|
11010
|
+
if (this.adminTokens === null) throw new Error("Admin tokens not available");
|
|
11011
|
+
return this.adminTokens.create(input);
|
|
11012
|
+
},
|
|
11013
|
+
validate: async (token) => {
|
|
11014
|
+
if (this.adminTokens === null || this.userManager === null) return null;
|
|
11015
|
+
const record = await this.adminTokens.validate(token);
|
|
11016
|
+
if (record === null) return null;
|
|
11017
|
+
const creator = await this.userManager.findById(record.createdBy);
|
|
11018
|
+
return creator !== null && creator.isAdmin ? record : null;
|
|
11019
|
+
},
|
|
11020
|
+
list: async () => this.adminTokens === null ? [] : this.adminTokens.list(),
|
|
11021
|
+
revoke: async (id) => this.adminTokens === null ? false : this.adminTokens.revoke(id)
|
|
11022
|
+
},
|
|
11023
|
+
audit: {
|
|
11024
|
+
record: async (event) => {
|
|
11025
|
+
await this.auditLog?.record(event);
|
|
11026
|
+
},
|
|
11027
|
+
list: async (input) => this.auditLog === null ? [] : this.auditLog.list(input)
|
|
11028
|
+
},
|
|
11029
|
+
setPassword: async (input) => {
|
|
11030
|
+
if (!this.userManager) throw new Error("User management not available");
|
|
11031
|
+
await this.userManager.resetPassword(input.userId, input.newPassword);
|
|
11032
|
+
await sessions().revokeAllForUser(input.userId, {
|
|
11033
|
+
...input.keepSessionId !== void 0 ? { exceptSessionId: input.keepSessionId } : {},
|
|
11034
|
+
reason: "password-changed"
|
|
11035
|
+
});
|
|
11036
|
+
},
|
|
11037
|
+
linkExternalIdentity: async (input) => {
|
|
11038
|
+
if (!this.externalIdentities || !this.userManager) throw new Error("External sign-in linking not available");
|
|
11039
|
+
if (!await this.userManager.findById(input.userId)) throw new Error(`User not found: ${input.userId}`);
|
|
11040
|
+
await this.externalIdentities.link(input);
|
|
11041
|
+
return { success: true };
|
|
11042
|
+
}
|
|
11043
|
+
};
|
|
11044
|
+
}
|
|
11045
|
+
/**
|
|
11046
|
+
* End every way `userId` could still get in: login sessions (and the
|
|
11047
|
+
* pre-session tokens, via the cutoff), scoped tokens, OAuth account links,
|
|
11048
|
+
* the TOTP row and the external sign-in links. Each step is attempted even
|
|
11049
|
+
* if an earlier one fails, and a failure is logged — a half-cleaned user is
|
|
11050
|
+
* worse than a loud one.
|
|
11051
|
+
*/
|
|
11052
|
+
async endAllAccess(userId) {
|
|
11053
|
+
const steps = [
|
|
11054
|
+
["sessions", async () => this.loginSessions?.revokeAllForUser(userId, { reason: "user-deleted" })],
|
|
11055
|
+
["scoped tokens", async () => {
|
|
11056
|
+
for (const token of await this.scopedTokenManager?.listForUser(userId) ?? []) await this.scopedTokenManager?.revoke(token.id);
|
|
11057
|
+
}],
|
|
11058
|
+
["oauth sessions", async () => {
|
|
11059
|
+
for (const session of await this.oauthSessionManager?.list() ?? []) if (session.userId === userId && session.revokedAt === null) await this.oauthSessionManager?.markRevoked(session.id);
|
|
11060
|
+
}],
|
|
11061
|
+
["admin tokens", async () => this.adminTokens?.revokeAllCreatedBy(userId)],
|
|
11062
|
+
["totp", async () => this.totpManager?.disable(userId)],
|
|
11063
|
+
["sign-in links", async () => this.externalIdentities?.removeAllForUser(userId)]
|
|
11064
|
+
];
|
|
11065
|
+
for (const [what, step] of steps) try {
|
|
11066
|
+
await step();
|
|
11067
|
+
} catch (err) {
|
|
11068
|
+
this.ctx.logger.error(`deleting a user: could not remove its ${what}`, { meta: {
|
|
11069
|
+
userId,
|
|
11070
|
+
error: err instanceof Error ? err.message : String(err)
|
|
11071
|
+
} });
|
|
11072
|
+
}
|
|
11073
|
+
}
|
|
11074
|
+
/**
|
|
10346
11075
|
* Drop auth rows that are dead but were never removed: scoped tokens past
|
|
10347
11076
|
* their expiry, and OAuth sessions revoked longer ago than
|
|
10348
11077
|
* {@link OAUTH_REVOKED_RETENTION_MS}.
|
|
@@ -10356,9 +11085,13 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10356
11085
|
try {
|
|
10357
11086
|
const tokens = await this.scopedTokenManager?.reapExpired() ?? 0;
|
|
10358
11087
|
const sessions = await this.oauthSessionManager?.reapRevoked(Date.now() - OAUTH_REVOKED_RETENTION_MS) ?? 0;
|
|
10359
|
-
|
|
11088
|
+
const logins = await this.loginSessions?.reap() ?? 0;
|
|
11089
|
+
await this.adminTokens?.reap();
|
|
11090
|
+
await this.auditLog?.reap();
|
|
11091
|
+
if (tokens > 0 || sessions > 0 || logins > 0) this.ctx.logger.info("auth retention swept", { meta: {
|
|
10360
11092
|
expiredTokens: tokens,
|
|
10361
|
-
revokedSessions: sessions
|
|
11093
|
+
revokedSessions: sessions,
|
|
11094
|
+
deadLoginSessions: logins
|
|
10362
11095
|
} });
|
|
10363
11096
|
} catch (err) {
|
|
10364
11097
|
this.ctx.logger.warn("auth retention sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
@@ -10377,7 +11110,10 @@ var LocalAuthAddon = class extends BaseAddon {
|
|
|
10377
11110
|
this.scopedTokenManager = null;
|
|
10378
11111
|
this.oauthSessionManager = null;
|
|
10379
11112
|
this.externalIdentities = null;
|
|
11113
|
+
this.loginSessions = null;
|
|
11114
|
+
this.adminTokens = null;
|
|
11115
|
+
this.auditLog = null;
|
|
10380
11116
|
}
|
|
10381
11117
|
};
|
|
10382
11118
|
//#endregion
|
|
10383
|
-
export { LocalAuthAddon, LocalAuthAddon as default,
|
|
11119
|
+
export { LocalAuthAddon, LocalAuthAddon as default, isSettingsReadUnavailable as _, attachUserManagementInternal as a, ADMIN_TOKEN_MAX_LIFETIME_MS as c, LoginSessionManager as d, REFRESH_TOKEN_PREFIX as f, SettingsReadUnavailableError as g, parseTokenExpiry as h, USER_MANAGEMENT_INTERNAL as i, ADMIN_TOKEN_PREFIX as l, AuthManager as m, FIRST_BOOT_ADMIN_USERNAME as n, getUserManagementInternal as o, ScopedTokenManager as p, UserManager as r, AuthAuditLog as s, FIRST_BOOT_ADMIN_PASSWORD as t, AdminTokenManager as u, ApiKeyManager as v };
|